diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..4bec4ea --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListFragment.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListFragment.kt index 093b01f..3a40740 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListFragment.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListFragment.kt @@ -11,12 +11,15 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch import ru.otus.cryptosample.CoinsSampleApp import ru.otus.cryptosample.coins.feature.adapter.CoinsAdapter import ru.otus.cryptosample.coins.feature.di.DaggerCoinListComponent import ru.otus.cryptosample.databinding.FragmentCoinListBinding import javax.inject.Inject +import ru.otus.cryptosample.R +import ru.otus.cryptosample.coins.feature.adapter.CoinItemAnimator class CoinListFragment : Fragment() { @@ -59,14 +62,15 @@ class CoinListFragment : Fragment() { } private fun setupRecyclerView() { - coinsAdapter = CoinsAdapter() + coinsAdapter = CoinsAdapter(RecyclerView.RecycledViewPool()) val gridLayoutManager = GridLayoutManager(requireContext(), 2) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (coinsAdapter.getItemViewType(position)) { - 0 -> 2 // Category header spans full width - 1 -> 1 // Coin item spans half width + R.layout.item_category_header -> 2 // Category header spans full width + R.layout.item_coin -> 1 // Coin item spans half-width + R.layout.item_carousel -> 2 // The horizontal row spans full width else -> 1 } } @@ -75,6 +79,15 @@ class CoinListFragment : Fragment() { binding.recyclerView.apply { layoutManager = gridLayoutManager adapter = coinsAdapter + itemAnimator = CoinItemAnimator() + } + + binding.btnAddCoin.setOnClickListener { + viewModel.addRandomCoin() + } + + binding.btnRemoveCoins.setOnClickListener { + viewModel.removeRandomCoin() } } @@ -99,7 +112,7 @@ class CoinListFragment : Fragment() { } private fun renderState(state: CoinsScreenState) { - coinsAdapter.setData(state.categories) + coinsAdapter.setData(state.categories, state.showAll) } override fun onDestroyView() { diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListViewModel.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListViewModel.kt index 6851a32..2897d60 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListViewModel.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinListViewModel.kt @@ -38,6 +38,50 @@ class CoinListViewModel( updateUiState() } + fun addRandomCoin() { + if (fullCategories.isEmpty()) return + + val allCoins = fullCategories.flatMap { it.coins } + if (allCoins.isEmpty()) return + + val templateCoin = allCoins.random() + + val newCoinId = "new_${System.currentTimeMillis()}" + val newCoin = templateCoin.copy( + id = newCoinId, + name = "New Coin ${(1..100).random()}", + price = "$${(1000..50000).random()}", + ) + + val randomCategory = fullCategories.random() + + fullCategories = fullCategories.map { category -> + if (category.id == randomCategory.id) { + category.copy(coins = category.coins + newCoin) + } else { + category + } + } + updateUiState() + } + + fun removeRandomCoin() { + if (fullCategories.isEmpty()) return + + val allCoins = fullCategories.flatMap { it.coins } + if (allCoins.isEmpty()) return + + val coinsToRemove = allCoins.shuffled().take(5.coerceAtMost(allCoins.size)) + val coinsToRemoveIds = coinsToRemove.map { it.id }.toSet() + + fullCategories = fullCategories.map { category -> + category.copy( + coins = category.coins.filter { coin -> coin.id !in coinsToRemoveIds } + ) + } + updateUiState() + } + private fun requestCoins() { consumeCoinsUseCase().map { categories -> categories.map { category -> coinsStateFactory.create(category) } @@ -54,20 +98,20 @@ class CoinListViewModel( } private fun updateUiState() { - var processedCategories = if (showAll) { - fullCategories - } else { - fullCategories.map { category -> - category.copy(coins = category.coins.take(4)) - } - } + var processedCategories = fullCategories processedCategories = processedCategories.map { category -> - category.copy(coins = category.coins.map { coin -> - coin.copy(highlight = highlightMovers && coin.isHotMover) - }) + category.copy( + coins = category.coins.map { coin -> + coin.copy(highlight = highlightMovers && coin.isHotMover) + }) } - _state.update { it.copy(categories = processedCategories) } + _state.update { + it.copy( + categories = processedCategories, + showAll = showAll + ) + } } } diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinState.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinState.kt index cecf2b7..483bbb8 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinState.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/CoinState.kt @@ -2,7 +2,8 @@ package ru.otus.cryptosample.coins.feature data class CoinsScreenState( val categories: List = emptyList(), -) + val showAll: Boolean = true, + ) data class CoinCategoryState( val id: String, diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinItemAnimator.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinItemAnimator.kt new file mode 100644 index 0000000..d3c8957 --- /dev/null +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinItemAnimator.kt @@ -0,0 +1,71 @@ +package ru.otus.cryptosample.coins.feature.adapter + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.view.View +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.RecyclerView + +class CoinItemAnimator : DefaultItemAnimator() { + + override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean { + holder.itemView.alpha = 0f + holder.itemView.translationX = -holder.itemView.width.toFloat() + + val fadeIn = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f, 1f) + val slideIn = ObjectAnimator.ofFloat(holder.itemView, View.TRANSLATION_X, -holder.itemView.width.toFloat(), 0f) + + val animatorSet = AnimatorSet() + animatorSet.playTogether(fadeIn, slideIn) + animatorSet.duration = addDuration + animatorSet.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + dispatchAddStarting(holder) + } + + override fun onAnimationEnd(animation: Animator) { + dispatchAddFinished(holder) + holder.itemView.alpha = 1f + holder.itemView.translationX = 0f + } + + override fun onAnimationCancel(animation: Animator) { + holder.itemView.alpha = 1f + holder.itemView.translationX = 0f + } + }) + animatorSet.start() + + return false + } + + override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean { + val fadeOut = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f, 0f) + val slideOut = ObjectAnimator.ofFloat(holder.itemView, View.TRANSLATION_X, 0f, holder.itemView.width.toFloat()) + + val animatorSet = AnimatorSet() + animatorSet.playTogether(fadeOut, slideOut) + animatorSet.duration = removeDuration + animatorSet.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + dispatchRemoveStarting(holder) + } + + override fun onAnimationEnd(animation: Animator) { + dispatchRemoveFinished(holder) + holder.itemView.alpha = 1f + holder.itemView.translationX = 0f + } + + override fun onAnimationCancel(animation: Animator) { + holder.itemView.alpha = 1f + holder.itemView.translationX = 0f + } + }) + animatorSet.start() + + return false + } +} diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinViewHolder.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinViewHolder.kt index 729978e..8d91e60 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinViewHolder.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinViewHolder.kt @@ -12,6 +12,10 @@ class CoinViewHolder( private val binding: ItemCoinBinding ) : RecyclerView.ViewHolder(binding.root) { + sealed class Payload { + data class HighlightChanged(val isHighlighted: Boolean) : Payload() + } + fun bind(coin: CoinState) { with(binding) { coinName.text = coin.name @@ -33,4 +37,19 @@ class CoinViewHolder( fireBadge.isVisible = coin.highlight } } + + fun bind(coin: CoinState, payloads: List) { + if (payloads.isEmpty()) { + bind(coin) + return + } + + payloads.forEach { payload -> + when (payload) { + is Payload.HighlightChanged -> { + binding.fireBadge.isVisible = payload.isHighlighted + } + } + } + } } diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapter.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapter.kt index 9d6ab4f..158e478 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapter.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapter.kt @@ -1,72 +1,145 @@ package ru.otus.cryptosample.coins.feature.adapter -import android.view.LayoutInflater import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import ru.otus.cryptosample.R import ru.otus.cryptosample.coins.feature.CoinCategoryState +import ru.otus.cryptosample.databinding.ItemCarouselBinding import ru.otus.cryptosample.databinding.ItemCategoryHeaderBinding import ru.otus.cryptosample.databinding.ItemCoinBinding -class CoinsAdapter : RecyclerView.Adapter() { - +class CoinsAdapter( + private val sharedPool: RecyclerView.RecycledViewPool +) : RecyclerView.Adapter() { + companion object { - private const val VIEW_TYPE_CATEGORY = 0 - private const val VIEW_TYPE_COIN = 1 + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: CoinsAdapterItem, + newItem: CoinsAdapterItem + ): Boolean { + return when (oldItem) { + is CoinsAdapterItem.CategoryHeader if newItem is CoinsAdapterItem.CategoryHeader -> + oldItem.categoryName == newItem.categoryName + + is CoinsAdapterItem.CoinItem if newItem is CoinsAdapterItem.CoinItem -> + oldItem.coin.id == newItem.coin.id + + is CoinsAdapterItem.HorizontalCoinsRow if newItem is CoinsAdapterItem.HorizontalCoinsRow -> + oldItem.categoryName == newItem.categoryName + + else -> false + } + } + + override fun areContentsTheSame( + oldItem: CoinsAdapterItem, + newItem: CoinsAdapterItem + ): Boolean { + return oldItem == newItem + } + + override fun getChangePayload( + oldItem: CoinsAdapterItem, + newItem: CoinsAdapterItem + ): Any? { + if (oldItem is CoinsAdapterItem.CoinItem && newItem is CoinsAdapterItem.CoinItem) { + val oldCoin = oldItem.coin + val newCoin = newItem.coin + if (oldCoin.highlight != newCoin.highlight && + oldCoin.copy(highlight = newCoin.highlight) == newCoin) { + return CoinViewHolder.Payload.HighlightChanged(newCoin.highlight) + } + } + return null + } + } } - - private var items = listOf() - - fun setData(categories: List) { + + private val differ = AsyncListDiffer(this, DIFF_CALLBACK) + + fun setData(categories: List, showAll: Boolean) { val adapterItems = mutableListOf() - + categories.forEach { category -> adapterItems.add(CoinsAdapterItem.CategoryHeader(category.name)) - category.coins.forEach { coin -> - adapterItems.add(CoinsAdapterItem.CoinItem(coin)) + + val coins = category.coins + val shouldUseHorizontalRow = coins.size > 10 && !showAll + + if (shouldUseHorizontalRow) { + adapterItems.add( + CoinsAdapterItem.HorizontalCoinsRow( + categoryName = category.name, + coins = coins + ) + ) + } else { + coins.forEach { coin -> + adapterItems.add(CoinsAdapterItem.CoinItem(coin)) + } } } - - items = adapterItems - notifyDataSetChanged() + + differ.submitList(adapterItems) } - - override fun getItemCount(): Int = items.size - + + override fun getItemCount(): Int = differ.currentList.size + override fun getItemViewType(position: Int): Int { - return when (items[position]) { - is CoinsAdapterItem.CategoryHeader -> VIEW_TYPE_CATEGORY - is CoinsAdapterItem.CoinItem -> VIEW_TYPE_COIN + return when (differ.currentList[position]) { + is CoinsAdapterItem.CategoryHeader -> R.layout.item_category_header + is CoinsAdapterItem.CoinItem -> R.layout.item_coin + is CoinsAdapterItem.HorizontalCoinsRow -> R.layout.item_carousel } } - + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - VIEW_TYPE_CATEGORY -> CategoryHeaderViewHolder( - ItemCategoryHeaderBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) + R.layout.item_category_header -> CategoryHeaderViewHolder( + parent.inflateBinding(ItemCategoryHeaderBinding::inflate) ) - VIEW_TYPE_COIN -> CoinViewHolder( - ItemCoinBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) + R.layout.item_coin -> CoinViewHolder( + parent.inflateBinding(ItemCoinBinding::inflate) + ) + R.layout.item_carousel -> HorizontalCoinsRowViewHolder( + parent.inflateBinding(ItemCarouselBinding::inflate), + sharedPool ) else -> throw IllegalArgumentException("Unknown view type: $viewType") } } - + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (val item = items[position]) { + when (val item = differ.currentList[position]) { is CoinsAdapterItem.CategoryHeader -> { (holder as CategoryHeaderViewHolder).bind(item.categoryName) } is CoinsAdapterItem.CoinItem -> { (holder as CoinViewHolder).bind(item.coin) } + is CoinsAdapterItem.HorizontalCoinsRow -> { + (holder as HorizontalCoinsRowViewHolder).bind(item.coins) + } + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + when (val item = differ.currentList[position]) { + is CoinsAdapterItem.CoinItem -> { + (holder as CoinViewHolder).bind(item.coin, payloads) + } + else -> super.onBindViewHolder(holder, position, payloads) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapterItem.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapterItem.kt index c483d5e..e867b08 100644 --- a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapterItem.kt +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/CoinsAdapterItem.kt @@ -2,7 +2,14 @@ package ru.otus.cryptosample.coins.feature.adapter import ru.otus.cryptosample.coins.feature.CoinState -sealed class CoinsAdapterItem { - data class CategoryHeader(val categoryName: String) : CoinsAdapterItem() - data class CoinItem(val coin: CoinState) : CoinsAdapterItem() +sealed interface CoinsAdapterItem { + + data class CategoryHeader(val categoryName: String) : CoinsAdapterItem + + data class CoinItem(val coin: CoinState) : CoinsAdapterItem + + data class HorizontalCoinsRow( + val categoryName: String, + val coins: List + ) : CoinsAdapterItem } \ No newline at end of file diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsAdapter.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsAdapter.kt new file mode 100644 index 0000000..bd56590 --- /dev/null +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsAdapter.kt @@ -0,0 +1,72 @@ +package ru.otus.cryptosample.coins.feature.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.roundToInt +import ru.otus.cryptosample.coins.feature.CoinState +import ru.otus.cryptosample.databinding.ItemCoinBinding + +class HorizontalCoinsAdapter : ListAdapter(DIFF_CALLBACK) { + + companion object { + private const val VISIBLE_ITEMS_COUNT = 2.25f + + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CoinState, newItem: CoinState): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: CoinState, newItem: CoinState): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: CoinState, newItem: CoinState): Any? { + return if (oldItem.highlight != newItem.highlight && + oldItem.copy(highlight = newItem.highlight) == newItem) { + CoinViewHolder.Payload.HighlightChanged(newItem.highlight) + } else { + null + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinViewHolder { + val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + val rv = parent as? RecyclerView + val parentWidthPx = rv?.width?.takeIf { it > 0 } + ?: parent.measuredWidth.takeIf { it > 0 } + ?: parent.resources.displayMetrics.widthPixels + + val paddingStart = rv?.paddingStart ?: 0 + val paddingEnd = rv?.paddingEnd ?: 0 + val availableWidth = (parentWidthPx - paddingStart - paddingEnd).coerceAtLeast(0) + + val itemWidth = (availableWidth / VISIBLE_ITEMS_COUNT).roundToInt().coerceAtLeast(1) + + val lp = (binding.root.layoutParams as? RecyclerView.LayoutParams) + ?: RecyclerView.LayoutParams(itemWidth, RecyclerView.LayoutParams.WRAP_CONTENT) + + lp.width = itemWidth + lp.height = RecyclerView.LayoutParams.WRAP_CONTENT + binding.root.layoutParams = lp + + return CoinViewHolder(binding) + } + + override fun onBindViewHolder(holder: CoinViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun onBindViewHolder(holder: CoinViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + holder.bind(getItem(position), payloads) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsRowViewHolder.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsRowViewHolder.kt new file mode 100644 index 0000000..bcfb79b --- /dev/null +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/HorizontalCoinsRowViewHolder.kt @@ -0,0 +1,26 @@ +package ru.otus.cryptosample.coins.feature.adapter + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cryptosample.coins.feature.CoinState +import ru.otus.cryptosample.databinding.ItemCarouselBinding + +class HorizontalCoinsRowViewHolder( + binding: ItemCarouselBinding, + sharedPool: RecyclerView.RecycledViewPool +) : RecyclerView.ViewHolder(binding.root) { + + private val adapter = HorizontalCoinsAdapter() + + init { + binding.horizontalRecyclerView.apply { + layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false) + setRecycledViewPool(sharedPool) + this.adapter = this@HorizontalCoinsRowViewHolder.adapter + } + } + + fun bind(coins: List) { + adapter.submitList(coins) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/ViewBindingExtensions.kt b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/ViewBindingExtensions.kt new file mode 100644 index 0000000..2700714 --- /dev/null +++ b/app/src/main/java/ru/otus/cryptosample/coins/feature/adapter/ViewBindingExtensions.kt @@ -0,0 +1,19 @@ +package ru.otus.cryptosample.coins.feature.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding + +/** + * Extension-функция для упрощения inflate ViewBinding в адаптерах. + * + * Пример использования: + * ``` + * parent.inflateBinding(ItemCoinBinding::inflate) + * ``` + */ +inline fun ViewGroup.inflateBinding( + crossinline bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> T +): T { + return bindingInflater(LayoutInflater.from(context), this, false) +} diff --git a/app/src/main/res/layout/fragment_coin_list.xml b/app/src/main/res/layout/fragment_coin_list.xml index b222909..5936f40 100644 --- a/app/src/main/res/layout/fragment_coin_list.xml +++ b/app/src/main/res/layout/fragment_coin_list.xml @@ -1,7 +1,6 @@ @@ -12,23 +11,44 @@ android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" - android:text="Coins" + android:text="@string/coins" + android:textColor="?attr/colorOnSurface" android:textSize="20sp" android:textStyle="bold" - android:textColor="?attr/colorOnSurface" - app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/btnAddCoin"> diff --git a/app/src/main/res/layout/item_carousel.xml b/app/src/main/res/layout/item_carousel.xml new file mode 100644 index 0000000..ec2ad37 --- /dev/null +++ b/app/src/main/res/layout/item_carousel.xml @@ -0,0 +1,7 @@ + + \ 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 5f2b821..358603a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,9 @@ CryptoSample Coins Main + Coins + Highlight movers + Show All + Add Coin + Remove Coins \ No newline at end of file