Koin
di/AppModule
val appModule = module { }
Application
class ShoppingAppApplication: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger(Level.ERROR)
androidContext(this@ShoppingAppApplication)
modules(appModule)
}
}
}
Data 영역
entity/ProductEntity
@Entity
data class ProductEntity(
@PrimaryKey val id: Long,
val createdAt: Date,
val productName: String,
val productPrice: Int,
val productImage: String,
val productType: String,
val productIntroductionImage: String
)
network/Url
object Url {
const val PRODUCT_BASE_URL = ""
}
network/ProductApiService
interface ProductApiService {
@GET("products")
suspend fun getProducts(): Response<ProductsResponse>
@GET("products/{productId}")
suspend fun getProduct(@Path("productId") productId: Long): Response<ProductResponse>
}
network/ProvideAPI
internal fun provideProductApiService(retrofit: Retrofit): ProductApiService {
return retrofit.create(ProductApiService::class.java)
}
internal fun provideProductRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory,
): Retrofit {
return Retrofit.Builder()
.baseUrl(Url.PRODUCT_BASE_URL)
.addConverterFactory(gsonConverterFactory)
.client(okHttpClient)
.build()
}
internal fun provideGsonConverterFactory(): GsonConverterFactory {
return GsonConverterFactory.create(
GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create()
)
}
internal fun buildOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
interceptor.level = HttpLoggingInterceptor.Level.NONE
}
return OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
}
response/ProductResponse
data class ProductResponse(
val id: String,
val createdAt: Long,
val productName: String,
val productPrice: Int,
val productImage: String,
val productType: String,
val productIntroductionImage: String
) {
fun toEntity(): ProductEntity =
ProductEntity(
id = id.toLong(),
createdAt = Date(createdAt),
productName = productName,
productPrice = productPrice.toDouble().toInt(),
productImage = productImage,
productType = productType,
productIntroductionImage = productIntroductionImage
)
}
response/ProductsResponse
data class ProductsResponse(
val items: List<ProductResponse>,
val count: Int
)
아이템 리스트를 보여주는 ProductsResponse와 하나의 아이템만 보여주는 ProductResponse 생성.
repository/ProductRepository
interface ProductRepository {
suspend fun getProductList(): List<ProductEntity>
suspend fun getLocalProductList(): List<ProductEntity>
suspend fun insertProductItem(ProductItem: ProductEntity): Long
suspend fun insertProductList(ProductList: List<ProductEntity>)
suspend fun updateProductItem(ProductItem: ProductEntity)
suspend fun getProductItem(itemId: Long): ProductEntity?
suspend fun deleteAll()
suspend fun deleteProductItem(id: Long)
}
repository/DefaultProductRepsoitory
class DefaultProductRepository(
private val productApi: ProductApiService,
private val ioDispatcher: CoroutineDispatcher
): ProductRepository { }
Domain 영역
UseCase
interface UseCase { }
GetProductItemUseCase
internal class GetProductItemUseCase(
private val productRepository: ProductRepository
): UseCase {
suspend operator fun invoke(productId: Long): ProductEntity? {
return productRepository.getProductItem(productId)
}
}
GetProductListUseCase
internal class GetProductListUseCase(
private val productRepository: ProductRepository
): UseCase {
suspend operator fun invoke(): List<ProductEntity> {
return productRepository.getProductList()
}
}
다른 UseCase들은 추후 구현
Presentation 영역
base/BaseViewModel
internal abstract class BaseViewModel: ViewModel() {
abstract fun fetchData(): Job
}
base/BaseFragment
internal abstract class BaseFragment<VM: BaseViewModel, VB: ViewBinding>: Fragment() {
abstract val viewModel: VM
protected lateinit var binding: VB
abstract fun getViewBinding(): VB
private lateinit var fetchJob: Job
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = getViewBinding()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fetchJob = viewModel.fetchData()
observeData()
}
abstract fun observeData()
override fun onDestroyView() {
super.onDestroyView()
if (fetchJob.isActive) {
fetchJob.cancel()
}
}
}
base/BaseActivity
internal abstract class BaseActivity<VM: BaseViewModel, VB: ViewBinding>: AppCompatActivity() {
abstract val viewModel: VM
protected lateinit var binding: VB
abstract fun getViewBinding(): VB
private lateinit var fetchJob: Job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = getViewBinding()
setContentView(binding.root)
fetchJob = viewModel.fetchData()
observeData()
}
abstract fun observeData()
override fun onDestroy() {
if (fetchJob.isActive) {
fetchJob.cancel()
}
super.onDestroy()
}
}
레이아웃 구성
activity_main.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="match_parent"
tools:context=".presentation.main.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fragmentContainer"
app:menu="@menu/bottom_navi_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
메인 화면 구현
MainActivity
internal class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), BottomNavigationView.OnNavigationItemSelectedListener {
override val viewModel by viewModel<MainViewModel>()
override fun getViewBinding(): ActivityMainBinding =
ActivityMainBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
}
private fun initViews() = with(binding) {
bottomNav.setOnNavigationItemSelectedListener(this@MainActivity)
showFragment(HomeFragment(), HomeFragment.TAG)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_home -> {
showFragment(HomeFragment.newInstance(), HomeFragment.TAG)
true
}
R.id.menu_my -> {
showFragment(MyFragment.newInstance(), MyFragment.TAG)
true
}
else -> false
}
}
private fun showFragment(fragment: Fragment, tag: String) {
val findFragment = supportFragmentManager.findFragmentByTag(tag)
supportFragmentManager.fragments.forEach { fm ->
supportFragmentManager.beginTransaction().hide(fm).commit()
}
findFragment?.let {
supportFragmentManager.beginTransaction().show(it).commit()
} ?: kotlin.run {
supportFragmentManager.beginTransaction()
.add(R.id.fragmentContainer, fragment, tag)
.commitAllowingStateLoss()
}
}
override fun observeData() {
}
}
MyFragment에 관한 코드는 My탭 구현 시 작성
MainViewModel
internal class MainViewModel: BaseViewModel() {
override fun fetchData(): Job = viewModelScope.launch {
}
}
HomeViewModel
internal class HomeViewModel(
private val getProductListUseCase: GetProductListUseCase
): BaseViewModel() {
private var _homeStateLiveData = MutableLiveData<HomeState>(HomeState.UnInitialized)
val homeStateLiveData: LiveData<HomeState> = _homeStateLiveData
override fun fetchData(): Job = viewModelScope.launch {
setState(
HomeState.Loading
)
setState(
HomeState.Success(
getProductListUseCase()
)
)
}
private fun setState(state: HomeState) {
_homeStateLiveData.postValue(state)
}
}
HomeFragment
internal class HomeFragment: BaseFragment<HomeViewModel, FragmentHomeBinding>() {
companion object {
fun newInstance() = HomeFragment()
const val TAG = "HomeFragment"
}
override fun getViewBinding(): FragmentHomeBinding = FragmentHomeBinding.inflate(layoutInflater)
private val adapter = ProductListAdapter()
private val startProductDetailForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
// TODO 성공적으로 처리 완료 이후 동작
}
override val viewModel by viewModel <HomeViewModel>()
override fun observeData() {
viewModel.homeStateLiveData.observe(this) {
when (it) {
is HomeState.UnInitialized -> {
initViews(binding)
}
is HomeState.Loading -> {
handleLoadingState()
}
is HomeState.Success -> {
handleSuccessState(it)
}
is HomeState.Error -> {
handleErrorState()
}
}
}
}
private fun initViews(binding: FragmentHomeBinding) = with(binding) {
recyclerView.adapter = adapter
refreshLayout.setOnRefreshListener {
viewModel.fetchData()
}
}
private fun handleLoadingState() = with(binding) {
refreshLayout.isRefreshing = true
}
private fun handleSuccessState(state: HomeState.Success) = with(binding) {
refreshLayout.isEnabled = state.productList.isNotEmpty()
refreshLayout.isRefreshing = false
if (state.productList.isEmpty()) {
emptyResultTextView.isGone = false
recyclerView.isGone = true
} else {
emptyResultTextView.isGone = true
recyclerView.isGone = false
adapter.setProductList(state.productList) {
// startProductDetailForResult.launch(
// ProductDetailActivity.newIntent(requireContext(), it.id)
// )
requireContext().toast("Product Entity : $it")
}
}
}
private fun handleErrorState() {
Toast.makeText(requireContext(), "에러가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
}
HomeState
sealed class HomeState {
object UnInitialized: HomeState()
object Loading: HomeState()
data class Success(
val productList: List<ProductEntity>
): HomeState()
object Error: HomeState()
}
adapter/ProductListAdapter
class ProductListAdapter : RecyclerView.Adapter<ProductListAdapter.ProductItemViewHolder>() {
private var productList: List<ProductEntity> = listOf()
private lateinit var productItemClickListener: (ProductEntity) -> Unit
inner class ProductItemViewHolder(
private val binding: ViewholderProductItemBinding,
val productItemClickListener: (ProductEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bindData(data: ProductEntity) = with(binding) {
productNameTextView.text = data.productName
productImageView.loadCenterCrop(data.productImage, 8f)
productPriceTextView.text = "${data.productPrice}원"
}
fun bindViews(data: ProductEntity) {
binding.root.setOnClickListener {
productItemClickListener(data)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductItemViewHolder {
val view = ViewholderProductItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ProductItemViewHolder(view, productItemClickListener)
}
override fun onBindViewHolder(holder: ProductItemViewHolder, position: Int) {
holder.bindData(productList[position])
holder.bindViews(productList[position])
}
override fun getItemCount(): Int = productList.size
@SuppressLint("NotifyDataSetChanged")
fun setProductList(productList: List<ProductEntity>, productItemClickListener: (ProductEntity) -> Unit = { }) {
this.productList = productList
this.productItemClickListener = productItemClickListener
notifyDataSetChanged()
}
}
viewholder_product_item.xml 과 extensions에 관한 것들은 생략했음(깃허브 참고)
repository/DefaultProductRepository
override suspend fun getProductList(): List<ProductEntity> = withContext(ioDispatcher) {
val response = productApi.getProducts()
return@withContext if (response.isSuccessful) {
response.body()?.items?.map { it.toEntity() } ?: listOf()
} else {
listOf()
}
}
di/AppModule
val appModule = module {
// Coroutines Dispatcher
single { Dispatchers.Main }
single { Dispatchers.IO }
// UseCase
factory { GetProductItemUseCase(get()) }
factory { GetProductListUseCase(get()) }
// Repository
single<ProductRepository> { DefaultProductRepository(get(), get()) }
single { provideGsonConverterFactory() }
single { buildOkHttpClient() }
single { provideProductRetrofit(get(), get()) }
single { provideProductApiService(get()) }
// viewModel
viewModel { MyViewModel() }
viewModel { HomeViewModel(get()) }
viewModel { MainViewModel() }
}
DB 관련
ProductDatabase
@Database(
entities = [ProductEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(DateConverter::class)
abstract class ProductDatabase: RoomDatabase() {
companion object {
const val DB_NAME = "ProductDataBase.db"
}
abstract fun productDao(): ProductDao
}
dao/ProductDao
@Dao
interface ProductDao {
@Query("SELECT * FROM ProductEntity")
suspend fun getAll(): List<ProductEntity>
@Query("SELECT * FROM ProductEntity WHERE id=:id")
suspend fun getById(id: Long): ProductEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(ProductEntity: ProductEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(ProductEntityList: List<ProductEntity>)
@Query("DELETE FROM ProductEntity WHERE id=:id")
suspend fun delete(id: Long)
@Query("DELETE FROM ProductEntity")
suspend fun deleteAll()
@Update
suspend fun update(ProductEntity: ProductEntity)
}
ProvideDB
internal fun provideDB(context: Context): ProductDatabase =
Room.databaseBuilder(context, ProductDatabase::class.java, ProductDatabase.DB_NAME).build()
internal fun provideToDoDao(database: ProductDatabase) = database.productDao()
utillity/DataConverter
object DateConverter {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
return if (dateLong == null) null else Date(dateLong)
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
}
데이터를 변환해 쉽게 저장할 수 있도록 DataConverter 만들어 처리
상세화면 구현
ProductDetailViewModel
internal class ProductDetailViewModel(
private val productId: Long,
private val getProductItemUseCase: GetProductItemUseCase
): BaseViewModel() {
private var _productDetailStateLiveData = MutableLiveData<ProductDetailState>(ProductDetailState.UnInitialized)
val productDetailStateLiveData: LiveData<ProductDetailState> = _productDetailStateLiveData
private lateinit var productEntity: ProductEntity
override fun fetchData(): Job = viewModelScope.launch {
setState(ProductDetailState.Loading)
getProductItemUseCase(productId)?.let { product ->
productEntity = product
setState(
ProductDetailState.Success(product)
)
} ?: kotlin.run {
setState(ProductDetailState.Error)
}
}
private fun setState(state: ProductDetailState) {
_productDetailStateLiveData.postValue(state)
}
}
ProductDetailActivity
internal class ProductDetailActivity : BaseActivity<ProductDetailViewModel, ActivityProductDetailBinding>() {
companion object {
const val PRODUCT_ID_KEY = "PRODUCT_ID_KEY"
const val PRODUCT_ORDERED_RESULT_CODE = 99
fun newIntent(context: Context, productId: Long) =
Intent(context, ProductDetailActivity::class.java).apply {
putExtra(PRODUCT_ID_KEY, productId)
}
}
override val viewModel by viewModel <ProductDetailViewModel> {
parametersOf(
intent.getLongExtra(PRODUCT_ID_KEY, -1)
)
}
override fun getViewBinding(): ActivityProductDetailBinding = ActivityProductDetailBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun observeData() = viewModel.productDetailStateLiveData.observe(this){
when (it) {
is ProductDetailState.UnInitialized -> initViews()
is ProductDetailState.Loading -> handleLoading()
is ProductDetailState.Success -> handleSuccess(it)
is ProductDetailState.Error -> handleError()
is ProductDetailState.Order -> handleOrder()
}
}
private fun initViews() = with(binding) {
setSupportActionBar(toolbar)
actionBar?.setDisplayHomeAsUpEnabled(true)
actionBar?.setDisplayShowHomeEnabled(true)
title = ""
toolbar.setNavigationOnClickListener {
finish()
}
orderButton.setOnClickListener {
viewModel.orderProduct()
}
}
private fun handleLoading() = with(binding) {
progressBar.isVisible = true
}
@SuppressLint("SetTextI18n")
private fun handleSuccess(state: ProductDetailState.Success) = with(binding) {
progressBar.isGone = true
val product = state.productEntity
title = product.productName
productCategoryTextView.text = product.productType
productImageView.loadCenterCrop(product.productImage, 8f)
productPriceTextView.text = "${product.productPrice}원"
introductionImageView.load(state.productEntity.productImage)
}
private fun handleError() {
toast("제품 정보를 불러올 수 없습니다.")
finish()
}
private fun handleOrder() {
setResult(PRODUCT_ORDERED_RESULT_CODE)
toast("성공적으로 주문이 완료되었습니다.")
finish()
}
}
ProductDetailState
sealed class ProductDetailState {
object UnInitialized: ProductDetailState()
object Loading: ProductDetailState()
data class Success(
val productEntity: ProductEntity
): ProductDetailState()
object Order: ProductDetailState()
object Error: ProductDetailState()
}
HomeFragment
adapter.setProductList(state.productList) {
startProductDetailForResult.launch(
ProductDetailActivity.newIntent(requireContext(), it.id)
)
}
handleSuccessState() 안에 구현
repository/DefaultProductRepository
override suspend fun getProductItem(itemId: Long): ProductEntity? = withContext(ioDispatcher) {
val response = productApi.getProduct(itemId)
return@withContext if (response.isSuccessful) {
response.body()?.toEntity()
} else {
null
}
}
주문 기능 구현
domain/OrderProductItemUseCase
internal class OrderProductItemUseCase(
private val productRepository: ProductRepository
): UseCase {
suspend operator fun invoke(productEntity: ProductEntity): Long {
return productRepository.insertProductItem(productEntity)
}
}
detail/ProductDetailViewModel
fun orderProduct() = viewModelScope.launch {
if (::productEntity.isInitialized) {
val productId = orderProductItemUseCase(productEntity)
if (productEntity.id == productId) {
setState(ProductDetailState.Order)
}
} else {
setState(ProductDetailState.Error)
}
}
private fun setState(state: ProductDetailState) {
_productDetailStateLiveData.postValue(state)
}
main/MainActivity
override fun observeData() = viewModel.mainStateLiveData.observe(this) {
when(it) {
is MainState.RefreshOrderList -> {
binding.bottomNav.selectedItemId = R.id.menu_my
val fragment = supportFragmentManager.findFragmentByTag(MyFragment.TAG)
// TODO fragment BaseFragment 타입 캐스팅 fetchData()
}
}
}
main/MainState
sealed class MainState {
object RefreshOrderList: MainState()
}
main/MainViewModel
internal class MainViewModel: BaseViewModel() {
override fun fetchData(): Job = viewModelScope.launch {}
private var _mainStateLiveData = MutableLiveData<MainState>()
val mainStateLiveData: LiveData<MainState> = _mainStateLiveData
fun refreshOrderList() = viewModelScope.launch {
_mainStateLiveData.postValue(MainState.RefreshOrderList)
}
}
home/HomeFragment
private val startProductDetailForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == ProductDetailActivity.PRODUCT_ORDERED_RESULT_CODE) {
(requireActivity() as MainActivity).viewModel.refreshOrderList()
}
}
repository/DefaultProductRepository
override suspend fun insertProductItem(ProductItem: ProductEntity): Long = withContext(ioDispatcher) {
productDao.insert(ProductItem)
}
di/AppModule
val appModule = module {
// UseCase
factory { OrderProductItemUseCase(get()) }
// Repository
single<ProductRepository> { DefaultProductRepository(get(), get(), get()) }
// viewModel
viewModel { (productId: Long) -> ProductDetailViewModel(productId, get(), get()) }
// Database
single { provideDB(androidApplication()) }
single { provideToDoDao(get()) }
}
AndroidManifest.xml
<activity
android:name=".presentation.detail.ProductDetailActivity"
android:theme="@style/AppTheme.PopupOverlay"
android:exported="false" />
Firebase console 구글로그인 설정 시 디버그 서명서 인증 필요
Android Studio에서 디버그 서명 인증서 SHA-1확인하는 법
1. 우측 상단 코끼리 아이콘의 Gradle
2. Tasks -> android -> signingReport
*실행 시 AVD 뜨지 않는 경우, Run -> Run -> app 선택하고 실행하면 됨
data/PreferenceManager
/**
* 데이터 저장 및 로드 클래스
*/
class PreferenceManager(
private val context: Context
) { }
자세한 코드는 깃허브 참고
presentation/MyState
sealed class MyState {
object Uninitialized: MyState()
object Loading: MyState()
data class Login(
val idToken: String
): MyState()
sealed class Success: MyState() {
data class Registered(
val userName: String,
val profileImageUri: Uri?,
val productList: List<ProductEntity> = listOf()
): Success()
object NotRegistered: Success()
}
object Error: MyState()
}
presentation/MyViewModel
internal class MyViewModel(
private val preferenceManager: PreferenceManager
): BaseViewModel() {
private var _myStateLiveData = MutableLiveData<MyState>(MyState.Uninitialized)
val myStateLiveData: LiveData<MyState> = _myStateLiveData
override fun fetchData(): Job = viewModelScope.launch {
setState(MyState.Loading)
preferenceManager.getIdToken()?.let {
setState(
MyState.Login(it)
)
} ?: kotlin.run {
setState(
MyState.Success.NotRegistered
)
}
}
private fun setState(state: MyState) {
_myStateLiveData.postValue(state)
}
fun saveToken(idToken: String) = viewModelScope.launch {
withContext(Dispatchers.IO) {
preferenceManager.putIdToken(idToken)
fetchData()
}
}
fun setUserInfo(firebaseUser: FirebaseUser?) = viewModelScope.launch {
firebaseUser?.let { user ->
setState(
MyState.Success.Registered(
user.displayName ?: "익명",
user.photoUrl,
listOf()
)
)
} ?: kotlin.run {
setState(
MyState.Success.NotRegistered
)
}
}
}
presentation/MyFragment
internal class MyFragment: BaseFragment<MyViewModel, FragmentMyBinding>() {
companion object {
fun newInstance() = MyFragment()
const val TAG = "MyFragment"
}
override val viewModel by viewModel<MyViewModel>()
override fun getViewBinding(): FragmentMyBinding = FragmentMyBinding.inflate(layoutInflater)
private val gso: GoogleSignInOptions by lazy {
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
}
private val gsc by lazy { GoogleSignIn.getClient(requireActivity(), gso) }
private val firebaseAuth by lazy { FirebaseAuth.getInstance() }
private val loginLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
task.getResult(ApiException::class.java)?.let { account ->
Log.e(TAG, "firebaseAuthWithGoogle: ${account.id}")
viewModel.saveToken(account.idToken ?: throw Exception())
} ?: throw Exception()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun observeData() = viewModel.myStateLiveData.observe(this) {
when (it) {
is MyState.Uninitialized -> initViews()
is MyState.Loading -> handleLoadingState()
is MyState.Login -> handleLoginState(it)
is MyState.Success -> handleSuccessState(it)
is MyState.Error -> handleErrorState()
}
}
private fun initViews() = with(binding) {
loginButton.setOnClickListener {
signInGoogle()
}
logoutButton.setOnClickListener { }
}
private fun handleLoadingState() = with(binding) {
progressBar.isVisible = true
loginRequiredGroup.isGone = true
}
private fun handleLoginState(state: MyState.Login) = with(binding) {
val credential = GoogleAuthProvider.getCredential(state.idToken, null)
firebaseAuth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
val user = firebaseAuth.currentUser
viewModel.setUserInfo(user)
} else {
viewModel.setUserInfo(null)
}
}
}
private fun handleSuccessState(state: MyState.Success) = with(binding) {
progressBar.isGone = true
when (state) {
is MyState.Success.Registered -> {
handleRegisteredState(state)
}
is MyState.Success.NotRegistered -> {
profileGroup.isGone = true
loginRequiredGroup.isVisible = true
}
}
}
private fun handleRegisteredState(state: MyState.Success.Registered) = with(binding) {
profileGroup.isVisible = true
loginRequiredGroup.isGone = true
profileImageView.loadCenterCrop(state.profileImageUri.toString(), 60f)
userNameTextView.text = state.userName
if (state.productList.isEmpty()) {
emptyResultTextView.isGone = false
recyclerView.isGone = true
} else {
emptyResultTextView.isGone = true
recyclerView.isGone = false
}
}
private fun handleErrorState() {
requireContext().toast("에러가 발생했습니다.")
}
private fun signInGoogle() {
val signInIntent = gsc.signInIntent
loginLauncher.launch(signInIntent)
}
}
주문내역 구현
domain/GetOrderedProductListUseCase
internal class GetOrderedProductListUseCase(
private val productRepository: ProductRepository
): UseCase {
suspend operator fun invoke(): List<ProductEntity> {
return productRepository.getLocalProductList()
}
}
repository/DefaultProductRepository
override suspend fun getLocalProductList(): List<ProductEntity> = withContext(ioDispatcher) {
productDao.getAll()
}
presentation/MyViewModel
internal class MyViewModel(
private val getOrderedProductListUseCase: GetOrderedProductListUseCase
): BaseViewModel() {
fun setUserInfo(firebaseUser: FirebaseUser?) = viewModelScope.launch {
firebaseUser?.let { user ->
setState(
MyState.Success.Registered(
user.displayName ?: "익명",
user.photoUrl,
getOrderedProductListUseCase()
)
)
}
임시로 listOf() 했던 것을 getOrderedProductListUseCase() 로 변경
di/AppModule
// UseCase
factory { GetOrderedProductListUseCase(get()) }
// viewModel
viewModel { MyViewModel(get(), get()) }
presentation/MyFragment
private val adapter = ProductListAdapter()
private fun initViews() = with(binding) {
recyclerView.adapter = adapter
}
presentation/MainActiviity
override fun observeData() = viewModel.mainStateLiveData.observe(this) {
when(it) {
is MainState.RefreshOrderList -> {
binding.bottomNav.selectedItemId = R.id.menu_my
val fragment = supportFragmentManager.findFragmentByTag(MyFragment.TAG)
// fragment BaseFragment 타입 캐스팅 fetchData()
(fragment as? BaseFragment<*, *>)?.viewModel?.fetchData()
}
}
}
로그아웃 구현
domain/DeleteOrderedProductListUseCase
internal class DeleteOrderedProductListUseCase(
private val productRepository: ProductRepository
): UseCase {
suspend operator fun invoke() {
return productRepository.deleteAll()
}
}
repository/DefaultProductRepository
override suspend fun deleteAll() = withContext(ioDispatcher) {
productDao.deleteAll()
}
presentation/MyViewModel
internal class MyViewModel(
private val deleteOrderedProductListUseCase: DeleteOrderedProductListUseCase
): BaseViewModel() {
fun signOut() = viewModelScope.launch {
withContext(Dispatchers.IO) {
preferenceManager.removedIdToken()
}
deleteOrderedProductListUseCase()
fetchData()
}
}
di/AppModule
// UseCase
factory { DeleteOrderedProductListUseCase(get()) }
// viewModel
viewModel { MyViewModel(get(), get(), get()) }
로그아웃 하면 상품이 사라지도록 하였다.