diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 27939ec..da7ade3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.serialization) } android { @@ -33,6 +34,10 @@ android { kotlinOptions { jvmTarget = "17" } + buildFeatures { + buildConfig = true + viewBinding = true + } } dependencies { @@ -43,6 +48,12 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.lifecycle.runtime) + implementation(libs.retrofit) + implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit.kotlin.serialization) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) + implementation(libs.glide) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 213a50a..020a798 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } - val tabLayout: TabLayout = findViewById(R.id.tl_list_tab) - //init fragment replaceFragment(RecentReadCardListFragment()) - tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + binding.tlListTab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { when (tab.position) { 0 -> { diff --git a/app/src/main/java/com/yongjincompany/android_assignment/core/util/LifeCycle.kt b/app/src/main/java/com/yongjincompany/android_assignment/core/util/LifeCycle.kt new file mode 100644 index 0000000..e8ce8e6 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/core/util/LifeCycle.kt @@ -0,0 +1,31 @@ +package com.yongjincompany.android_assignment.core.util + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +fun Fragment.repeatOnLifecycleState( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +) = + viewLifecycleOwner.launchRepeatOnLifecycleState(state, block) + +fun AppCompatActivity.repeatOnLifecycleState( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +): Job = + launchRepeatOnLifecycleState(state, block) + +private fun LifecycleOwner.launchRepeatOnLifecycleState( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +) = + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(state, block) + } \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/core/util/Toast.kt b/app/src/main/java/com/yongjincompany/android_assignment/core/util/Toast.kt new file mode 100644 index 0000000..e0c1655 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/core/util/Toast.kt @@ -0,0 +1,8 @@ +package com.yongjincompany.android_assignment.core.util + +import android.content.Context +import android.widget.Toast + +fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, message, duration).show() +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/Card.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/Card.kt deleted file mode 100644 index c7c3424..0000000 --- a/app/src/main/java/com/yongjincompany/android_assignment/data/Card.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.yongjincompany.android_assignment.data - -import androidx.annotation.DrawableRes - -data class Card( - val id: Int, - val grade: Grade, - val name: String, - @DrawableRes val img: Int, - val description: String? = null -) - -enum class Grade { - S, - A, - B, - C, - D -} diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/RecentReadCardListManager.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/RecentReadCardListManager.kt deleted file mode 100644 index f4abaf1..0000000 --- a/app/src/main/java/com/yongjincompany/android_assignment/data/RecentReadCardListManager.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.yongjincompany.android_assignment.data - -object RecentReadCardListManager { - private const val NOT_CONTAIN_SUCH_ELEMENT = -1 - - private val _recentReadCardList = mutableListOf() - val recentReadCardList - get() = _recentReadCardList.toList() - - fun addRecentReadCard(card: Card) { - val sameCardElementIndex = _recentReadCardList.indexOfFirst { it.id == card.id } - - if (sameCardElementIndex == NOT_CONTAIN_SUCH_ELEMENT) { - _recentReadCardList.add(card) - } else { - _recentReadCardList.removeAt(sameCardElementIndex) - _recentReadCardList.add(0, card) - } - } - - fun removeRecentFirstReadCard(block: () -> Unit) { - if (_recentReadCardList.isNotEmpty()) { - _recentReadCardList.removeAt(0) - block() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/RepositoryBuilder.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/RepositoryBuilder.kt new file mode 100644 index 0000000..c25104b --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/data/RepositoryBuilder.kt @@ -0,0 +1,8 @@ +package com.yongjincompany.android_assignment.data + +import com.yongjincompany.android_assignment.data.RetrofitBuilder.cardApi +import com.yongjincompany.android_assignment.data.repository.CardRepository + +object RepositoryBuilder { + val cardRepository = CardRepository(cardApi) +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/RetrofitBuilder.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/RetrofitBuilder.kt new file mode 100644 index 0000000..4114d57 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/data/RetrofitBuilder.kt @@ -0,0 +1,39 @@ +package com.yongjincompany.android_assignment.data + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.yongjincompany.android_assignment.BuildConfig +import com.yongjincompany.android_assignment.data.api.CardApi +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit + +object RetrofitBuilder { + private const val BASE_URL = "http://10.0.2.2:8080/api/" + + private val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + } + + private val httpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .build() + + private val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + val cardApi: CardApi = retrofit.create(CardApi::class.java) +} diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/api/CardApi.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/api/CardApi.kt new file mode 100644 index 0000000..1952e7c --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/data/api/CardApi.kt @@ -0,0 +1,21 @@ +package com.yongjincompany.android_assignment.data.api + +import com.yongjincompany.android_assignment.data.model.response.Card +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.Path +import retrofit2.http.Query + +interface CardApi { + @GET("card") + suspend fun fetchAllCardList(): List + + @GET("card/read") + suspend fun fetchRecentReadCardList(): List + + @PATCH("card/read/{id}") + suspend fun changeCardReadStatus( + @Path("id") id: Long, + @Query("isRead") isRead: Boolean + ): Card +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/model/response/Card.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/model/response/Card.kt new file mode 100644 index 0000000..09992bc --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/data/model/response/Card.kt @@ -0,0 +1,22 @@ +package com.yongjincompany.android_assignment.data.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class Card( + val description: String, + val grade: CardGradeType, + val id: Long, + val imageUrl: String, + val isRead: Boolean, + val name: String, +) + +@Serializable +enum class CardGradeType { + S, + A, + B, + C, + D, +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/data/repository/CardRepository.kt b/app/src/main/java/com/yongjincompany/android_assignment/data/repository/CardRepository.kt new file mode 100644 index 0000000..245a6f5 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/data/repository/CardRepository.kt @@ -0,0 +1,18 @@ +package com.yongjincompany.android_assignment.data.repository + +import com.yongjincompany.android_assignment.data.api.CardApi +import com.yongjincompany.android_assignment.data.model.response.Card + +class CardRepository(private val cardApi: CardApi) { + suspend fun fetchAllCardList(): Result> = + kotlin.runCatching { cardApi.fetchAllCardList() } + + suspend fun fetchRecentReadCardList(): Result> = + kotlin.runCatching { cardApi.fetchRecentReadCardList() } + + suspend fun changeCardReadStatus( + id: Long, + isRead: Boolean + ): Result = + kotlin.runCatching { cardApi.changeCardReadStatus(id, isRead) } +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/AllCardListFragment.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/AllCardListFragment.kt index c59800b..ffd4a36 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/AllCardListFragment.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/AllCardListFragment.kt @@ -1,31 +1,64 @@ package com.yongjincompany.android_assignment.feature.home import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView -import com.yongjincompany.android_assignment.data.Card -import com.yongjincompany.android_assignment.data.Grade -import com.yongjincompany.android_assignment.R +import androidx.lifecycle.ViewModelProvider import com.yongjincompany.android_assignment.core.util.SpacingItemDecoration +import com.yongjincompany.android_assignment.core.util.repeatOnLifecycleState +import com.yongjincompany.android_assignment.data.RepositoryBuilder +import com.yongjincompany.android_assignment.databinding.FragmentAllCardListBinding import com.yongjincompany.android_assignment.feature.home.adapter.AllCardListAdapter +import com.yongjincompany.android_assignment.feature.home.viewmodel.AllCardListViewModel +import com.yongjincompany.android_assignment.feature.home.viewmodel.AllCardListViewModelFactory -class AllCardListFragment : Fragment(R.layout.fragment_all_card_list) { +class AllCardListFragment : Fragment() { + private var _binding: FragmentAllCardListBinding? = null + private val binding get() = _binding!! + private lateinit var allCardListAdapter: AllCardListAdapter + private lateinit var vm: AllCardListViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAllCardListBinding.inflate(inflater, container, false) + val view = binding.root + return view + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val allCardListRecyclerView: RecyclerView = view.findViewById(R.id.rv_all_card_list) - val allCardList = listOf( - Card(id = 0, grade = Grade.D, name = "사죄 용진", img = R.drawable.sorry_yongjin_card_img, description = "왜 이 사람은 책상 위에 올라가 사죄하고 있을까요? 전 알아요 그는 세미콜론이라는 동아리를 나가서 새 길을 개척하겠다는 포부를 가지고 있다는 것을"), - Card(id = 1, grade = Grade.C, name = "민달팽이 정민", img = R.drawable.snail_jeongmin_card_img, description = "문정민, 그는 프랭키 그 자체입니다. 그의 얼굴을 보고 절대 속으면 안됩니다. 끔찍하게도 그의 얼굴과 달리 그의 몸은 민달팽이와 같은 구조로 되어있습니다."), - Card(id = 2, grade = Grade.B, name = "이별 영준", img = R.drawable.brakeup_youngjun_card_img, description = "만약 저런 표정으로 누워있는 그를 발견한다면 그를 절대로 건드리지 않는 것이 좋을겁니다. 그는 안좋은 일을 겪었거든요…"), - Card(id = 3, grade = Grade.A, name = "마스터이 경수", img = R.drawable.masteryi_gyeongsu_card_img, description = "그는 잠자리일까요? 사람일까요? 그의 정체를 아는 사람은 없을겁니다… 아마도요.."), - Card(id = 4, grade = Grade.A, name = "앙큼한 진성", img = R.drawable.cute_jinsung_card_img, description = "오우 그를 보셨나요? 그는 참 상큼발랄한 친구입니다. 그의 이름이요? 황진성입니다. 그는 웹개발자이죠.") - ) - - allCardListRecyclerView.adapter = AllCardListAdapter(allCardList) - allCardListRecyclerView.addItemDecoration(SpacingItemDecoration(30)) + init() + observe() + } + + private fun init() { + val viewModelFactory = AllCardListViewModelFactory(RepositoryBuilder.cardRepository) + vm = ViewModelProvider(this, viewModelFactory)[AllCardListViewModel::class.java] + + allCardListAdapter = AllCardListAdapter() + binding.rvAllCardList.adapter = allCardListAdapter + binding.rvAllCardList.addItemDecoration(SpacingItemDecoration(30)) + + vm.fetchAllCardList() + } + + private fun observe() { + repeatOnLifecycleState { + vm.allCardList.collect { + allCardListAdapter.updateAllCard(vm.allCardList.value) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null } } diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/CardDetailActivity.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/CardDetailActivity.kt index 7718328..4a1e204 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/CardDetailActivity.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/CardDetailActivity.kt @@ -1,39 +1,67 @@ package com.yongjincompany.android_assignment.feature.home import android.os.Bundle -import android.widget.ImageView -import android.widget.TextView -import androidx.activity.ComponentActivity -import com.yongjincompany.android_assignment.R +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.bumptech.glide.Glide +import com.yongjincompany.android_assignment.core.util.repeatOnLifecycleState +import com.yongjincompany.android_assignment.core.util.toast +import com.yongjincompany.android_assignment.data.RepositoryBuilder +import com.yongjincompany.android_assignment.databinding.ActivityCardDetailBinding +import com.yongjincompany.android_assignment.feature.home.viewmodel.CardDetailFailed +import com.yongjincompany.android_assignment.feature.home.viewmodel.CardDetailViewModel +import com.yongjincompany.android_assignment.feature.home.viewmodel.CardDetailViewModelFactory +import com.yongjincompany.android_assignment.feature.home.viewmodel.ChangeCardReadStatusSuccess + +class CardDetailActivity : AppCompatActivity() { + private lateinit var binding: ActivityCardDetailBinding + private lateinit var vm : CardDetailViewModel -class CardDetailActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_card_detail) + binding = ActivityCardDetailBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) - val cardId: TextView = findViewById(R.id.detail_tv_card_id) - val cardName: TextView = findViewById(R.id.detail_tv_card_name) - val cardImg: ImageView = findViewById(R.id.detail_iv_card_img) - val cardGrade: TextView = findViewById(R.id.detail_tv_card_grade) - val cardDescription: TextView = findViewById(R.id.detail_tv_card_description) + init() + observe() + } - val backBtn: ImageView = findViewById(R.id.detail_iv_back) + private fun init() { + val receivedCardId = intent.getLongExtra("card_id", 0) - val receivedCardId = intent.getIntExtra("card_id", 0) - val receivedCardName = intent.getStringExtra("card_name") - val receivedCardImg = intent.getIntExtra("card_img", 0) - val receivedCardGrade = intent.getStringExtra("card_grade") - val receivedCardDescription = intent.getStringExtra("card_description") + val viewModelFactory = CardDetailViewModelFactory(RepositoryBuilder.cardRepository) + vm = ViewModelProvider(this, viewModelFactory)[CardDetailViewModel::class.java] - cardId.text = receivedCardId.toString() - cardName.text = receivedCardName - cardImg.setImageResource(receivedCardImg) - cardGrade.text = receivedCardGrade - cardDescription.text = receivedCardDescription + vm.changeCardReadStatus( + receivedCardId, + true + ) - backBtn.setOnClickListener { + binding.ivBack.setOnClickListener { finish() } + } + private fun observe() { + repeatOnLifecycleState { + vm.event.collect { + when(it) { + is ChangeCardReadStatusSuccess -> { + binding.tvCardId.text = it.data.id.toString() + binding.tvCardName.text = it.data.name + binding.tvCardGrade.text = it.data.grade.toString() + binding.tvCardDescription.text = it.data.description + Glide.with(this@CardDetailActivity) + .load(it.data.imageUrl) + .into(binding.ivCardImg) + } + + is CardDetailFailed -> { + baseContext.toast(getString(it.errorMessage)) + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/RecentReadCardListFragment.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/RecentReadCardListFragment.kt index c5a6ca6..e0f11c6 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/RecentReadCardListFragment.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/RecentReadCardListFragment.kt @@ -1,53 +1,95 @@ package com.yongjincompany.android_assignment.feature.home import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import android.widget.Toast +import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.RecyclerView -import com.yongjincompany.android_assignment.data.RecentReadCardListManager +import androidx.lifecycle.ViewModelProvider import com.yongjincompany.android_assignment.R import com.yongjincompany.android_assignment.core.util.SpacingItemDecoration +import com.yongjincompany.android_assignment.core.util.repeatOnLifecycleState +import com.yongjincompany.android_assignment.core.util.toast +import com.yongjincompany.android_assignment.data.RepositoryBuilder +import com.yongjincompany.android_assignment.databinding.FragmentRecentReadCardListBinding import com.yongjincompany.android_assignment.feature.home.adapter.RecentReadCardListAdapter +import com.yongjincompany.android_assignment.feature.home.viewmodel.ChangeReadStatusSuccess +import com.yongjincompany.android_assignment.feature.home.viewmodel.FetchRecentReadCardListSuccess +import com.yongjincompany.android_assignment.feature.home.viewmodel.RecentReadCardListFailed +import com.yongjincompany.android_assignment.feature.home.viewmodel.RecentReadCardListViewModel +import com.yongjincompany.android_assignment.feature.home.viewmodel.RecentReadCardListViewModelFactory import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -class RecentReadCardListFragment : Fragment(R.layout.fragment_recent_read_card_list) { - private var recentReadCardListAdapter: RecentReadCardListAdapter? = null +class RecentReadCardListFragment : Fragment() { + private var _binding: FragmentRecentReadCardListBinding? = null + private val binding get() = _binding!! + private lateinit var recentReadCardListAdapter: RecentReadCardListAdapter + private lateinit var vm: RecentReadCardListViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentRecentReadCardListBinding.inflate(inflater, container, false) + val view = binding.root + return view + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val recentReadCardListRecyclerView: RecyclerView = - view.findViewById(R.id.rv_recent_read_card_list) + init() + observe() + } + + private fun init() { + val viewModelFactory = RecentReadCardListViewModelFactory(RepositoryBuilder.cardRepository) + vm = ViewModelProvider(this, viewModelFactory)[RecentReadCardListViewModel::class.java] recentReadCardListAdapter = RecentReadCardListAdapter() - recentReadCardListRecyclerView.adapter = recentReadCardListAdapter - recentReadCardListRecyclerView.addItemDecoration(SpacingItemDecoration(30)) - - recentReadCardListAdapter?.submitList(RecentReadCardListManager.recentReadCardList) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { - while (true) { - delay(10000) - RecentReadCardListManager.removeRecentFirstReadCard( - { - recentReadCardListAdapter?.submitList(RecentReadCardListManager.recentReadCardList) - Toast - .makeText( - requireContext(), - getString(R.string.remove_card_notice), - Toast.LENGTH_SHORT - ) - .show() + binding.rvRecentReadCardList.adapter = recentReadCardListAdapter + binding.rvRecentReadCardList.addItemDecoration(SpacingItemDecoration(30)) + + vm.fetchRecentReadCardList() + } + + private fun observe() { + repeatOnLifecycleState { + vm.event.collect { + when (it) { + ChangeReadStatusSuccess -> { + recentReadCardListAdapter.removeFirstItem() + requireContext().toast(getString(R.string.remove_card_notice)) + } + + is FetchRecentReadCardListSuccess -> { + recentReadCardListAdapter.submitList(it.data) + + repeatOnLifecycleState(Lifecycle.State.RESUMED) { + while (true) { + delay(10000) + if (recentReadCardListAdapter.currentList.isNotEmpty()) { + vm.changeCardReadStatus( + recentReadCardListAdapter.currentList[0].id, + false + ) + } else { + break + } + } } - ) + } + + is RecentReadCardListFailed -> requireContext().toast(getString(it.errorMessage)) } } } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/AllCardListAdapter.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/AllCardListAdapter.kt index 4a8b30c..ba21439 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/AllCardListAdapter.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/AllCardListAdapter.kt @@ -4,27 +4,23 @@ import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.yongjincompany.android_assignment.data.RecentReadCardListManager -import com.yongjincompany.android_assignment.data.Card -import com.yongjincompany.android_assignment.R +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.databinding.ItemCardBinding import com.yongjincompany.android_assignment.feature.home.CardDetailActivity -class AllCardListAdapter(private val data: List) : +class AllCardListAdapter(private var data: List = emptyList()) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder { - val view = LayoutInflater - .from(parent.context) - .inflate(R.layout.item_card, parent, false) + val binding = ItemCardBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return CardViewHolder(view, { + return CardViewHolder(binding, { val cardData = data[it] - RecentReadCardListManager.addRecentReadCard(cardData) val intent = Intent(parent.context, CardDetailActivity::class.java) intent.apply { putExtra("card_id", cardData.id) putExtra("card_name", cardData.name) - putExtra("card_img", cardData.img) + putExtra("card_img", cardData.imageUrl) putExtra("card_grade", cardData.grade.name) putExtra("card_description", cardData.description) } @@ -37,4 +33,9 @@ class AllCardListAdapter(private val data: List) : } override fun getItemCount(): Int = data.size + + fun updateAllCard(cardList: List) { + this.data = cardList + notifyDataSetChanged() + } } diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/CardViewHolder.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/CardViewHolder.kt index ba8b163..a8e7e89 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/CardViewHolder.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/CardViewHolder.kt @@ -1,30 +1,23 @@ package com.yongjincompany.android_assignment.feature.home.adapter -import android.view.View -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.yongjincompany.android_assignment.R -import com.yongjincompany.android_assignment.data.Card +import com.bumptech.glide.Glide +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.databinding.ItemCardBinding -class CardViewHolder(itemView: View, private val listener: (Int) -> Unit) : ViewHolder(itemView) { - val cardName: TextView - val cardGrade: TextView - val cardImg: ImageView +class CardViewHolder(private val binding: ItemCardBinding, private val listener: (Int) -> Unit) : ViewHolder(binding.root) { init { - cardName = itemView.findViewById(R.id.tv_card_name) - cardGrade = itemView.findViewById(R.id.tv_card_grade) - cardImg = itemView.findViewById(R.id.iv_card_img) - itemView.setOnClickListener { listener(adapterPosition) } } fun bind(card: Card) { - cardImg.setImageResource(card.img) - cardName.text = card.name - cardGrade.text = card.grade.name + binding.tvCardName.text = card.name + binding.tvCardGrade.text = card.grade.name + Glide.with(binding.root.context) + .load(card.imageUrl) + .into(binding.ivCardImg) } } \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/RecentReadCardListAdapter.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/RecentReadCardListAdapter.kt index 3dc091a..6ebaf33 100644 --- a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/RecentReadCardListAdapter.kt +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/adapter/RecentReadCardListAdapter.kt @@ -4,15 +4,13 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.yongjincompany.android_assignment.data.Card -import com.yongjincompany.android_assignment.R +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.databinding.ItemCardBinding class RecentReadCardListAdapter : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder { - val view = LayoutInflater - .from(parent.context) - .inflate(R.layout.item_card, parent, false) - return CardViewHolder(view, {}) + val binding = ItemCardBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return CardViewHolder(binding, {}) } override fun onBindViewHolder(holder: CardViewHolder, position: Int) { @@ -36,4 +34,13 @@ class RecentReadCardListAdapter : ListAdapter(diffUtil) { } } } + + fun removeFirstItem() { + val currentList = currentList.toMutableList() + + if (currentList.isNotEmpty()) { + currentList.removeAt(0) + submitList(currentList) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModel.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModel.kt new file mode 100644 index 0000000..8dd548e --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModel.kt @@ -0,0 +1,23 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.data.repository.CardRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class AllCardListViewModel(private val cardRepository: CardRepository) : ViewModel() { + private val _allCardList = MutableStateFlow>(emptyList()) + val allCardList = _allCardList.asStateFlow() + + fun fetchAllCardList() { + viewModelScope.launch { + cardRepository.fetchAllCardList() + .onSuccess { + _allCardList.value = it + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModelFactory.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModelFactory.kt new file mode 100644 index 0000000..2c6f8ad --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/AllCardListViewModelFactory.kt @@ -0,0 +1,11 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.yongjincompany.android_assignment.data.repository.CardRepository + +class AllCardListViewModelFactory(private val cardRepository: CardRepository): ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return AllCardListViewModel(cardRepository) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModel.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModel.kt new file mode 100644 index 0000000..f58946e --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModel.kt @@ -0,0 +1,34 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.yongjincompany.android_assignment.R +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.data.repository.CardRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class CardDetailViewModel(private val cardRepository: CardRepository) : ViewModel() { + private val _event = MutableSharedFlow() + val event = _event.asSharedFlow() + + fun changeCardReadStatus(id: Long, isRead: Boolean) { + viewModelScope.launch { + cardRepository.changeCardReadStatus( + id, + isRead + ).onSuccess { + _event.emit(ChangeCardReadStatusSuccess(it)) + }.onFailure { + _event.emit(CardDetailFailed(R.string.cant_save_card_read)) + } + } + } +} + +sealed interface CardDetailEvent + +data class ChangeCardReadStatusSuccess(val data: Card) : CardDetailEvent +data class CardDetailFailed(@StringRes val errorMessage: Int) : CardDetailEvent \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModelFactory.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModelFactory.kt new file mode 100644 index 0000000..ec22047 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/CardDetailViewModelFactory.kt @@ -0,0 +1,11 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.yongjincompany.android_assignment.data.repository.CardRepository + +class CardDetailViewModelFactory(private val cardRepository: CardRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return CardDetailViewModel(cardRepository) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModel.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModel.kt new file mode 100644 index 0000000..3568f94 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModel.kt @@ -0,0 +1,46 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.yongjincompany.android_assignment.R +import com.yongjincompany.android_assignment.data.model.response.Card +import com.yongjincompany.android_assignment.data.repository.CardRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class RecentReadCardListViewModel(private val cardRepository: CardRepository) : ViewModel() { + private val _event = MutableSharedFlow() + val event = _event.asSharedFlow() + + fun fetchRecentReadCardList() { + viewModelScope.launch { + cardRepository.fetchRecentReadCardList() + .onSuccess { + _event.emit(FetchRecentReadCardListSuccess(it)) + }.onFailure { + _event.emit(RecentReadCardListFailed(R.string.cant_fetch_card_list)) + } + } + } + + fun changeCardReadStatus(id: Long, isRead: Boolean) { + viewModelScope.launch { + cardRepository.changeCardReadStatus( + id, + isRead + ).onSuccess { + _event.emit(ChangeReadStatusSuccess) + }.onFailure { + _event.emit(RecentReadCardListFailed(R.string.cant_save_card_read)) + } + } + } +} + +sealed interface RecentReadCardListEvent + +data class FetchRecentReadCardListSuccess(val data: List) : RecentReadCardListEvent +data object ChangeReadStatusSuccess : RecentReadCardListEvent +data class RecentReadCardListFailed(val errorMessage: Int) : RecentReadCardListEvent + diff --git a/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModelFactory.kt b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModelFactory.kt new file mode 100644 index 0000000..6a9cfd6 --- /dev/null +++ b/app/src/main/java/com/yongjincompany/android_assignment/feature/home/viewmodel/RecentReadCardListViewModelFactory.kt @@ -0,0 +1,11 @@ +package com.yongjincompany.android_assignment.feature.home.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.yongjincompany.android_assignment.data.repository.CardRepository + +class RecentReadCardListViewModelFactory(private val cardRepository: CardRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return RecentReadCardListViewModel(cardRepository) as T + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_card_detail.xml b/app/src/main/res/layout/activity_card_detail.xml index 7ef1309..12c6995 100644 --- a/app/src/main/res/layout/activity_card_detail.xml +++ b/app/src/main/res/layout/activity_card_detail.xml @@ -13,17 +13,17 @@ app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="@+id/tv_title" /> + app:layout_constraintStart_toEndOf="@+id/iv_card_img" + app:layout_constraintTop_toBottomOf="@+id/tv_card_name" /> + app:layout_constraintStart_toEndOf="@+id/iv_card_img" + app:layout_constraintTop_toBottomOf="@+id/tv_card_grade_title" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa5068b..07a501b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,10 +9,15 @@ 상세보기 카드 등급 카드 고유 번호 + 카드 읽기 상태를 저장할 수 없습니다. 다시 시도해주세요 + 최근 읽은 카드 목록을 받아올 수 없습니다. 다시 시도해주세요 10초가 지나 카드가 읽기 목록에서 사라집니다 + + 전체 카드 정보를 받아올 수 없습니다. 다시 시도해주세요 + 카드 이미지 앱 로고 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 744ac35..f6423f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,12 @@ material = "1.12.0" activity = "1.9.3" constraintlayout = "2.1.4" lifecycle = "2.8.7" +retrofit = "2.11.0" +kotlinxSerializationJson = "1.6.3" +retrofitKotlinxSerializationJson = "1.0.0" +okhttp = "4.12.0" +glide = "4.16.0" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -21,8 +27,15 @@ material = { group = "com.google.android.material", name = "material", version.r androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +retrofit = {group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit"} +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }