Valorant API Clean Architecture Android Kotlin App Development | Code

ibrahimcanerdogan
7 min readJan 11, 2024

--

Photo by Lorenzo Herrera on Unsplash

In the dynamic world of online gaming, mastering a title like Valorant requires more than just skill; it demands knowledge, strategy, and a keen understanding of the game’s nuances. For every aspiring Valorant enthusiast, the ‘Valorant Master Professional Guide’ app emerges as the indispensable tool to elevate your gameplay and dominate the competition.

Unlock the Secrets of Agents

At the heart of Valorant’s tactical gameplay are its diverse agents, each with unique abilities that can turn the tide of battle. The ‘Valorant Master Professional Guide’ app provides a deep dive into every agent’s strengths, weaknesses, and optimal strategies. Whether you’re a duelist or a support player, understanding the intricacies of your chosen agent is key to success.

API Service

    @GET("v1/agents")
suspend fun getAgentRemote() : Response<Agent>

Room Database

@Database(
entities = [AgentData::class, MapData::class, WeaponData::class],
version = 3,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class ValorantDatabase : RoomDatabase() {

abstract fun agentDao() : AgentDao

abstract fun mapDao() : MapDao

abstract fun weaponDao() : WeaponDao
}

@Dao
interface AgentDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAgentDatabase(agentList : List<AgentData>)

@Query("SELECT * FROM agent_data")
fun getAgentListDatabase() : List<AgentData>
}

Repository

class AgentRepositoryImpl @Inject constructor(
private val remoteDataSource: AgentRemoteDataSource,
private val localeDataSource: AgentLocaleDataSource
) : AgentRepository {

override suspend fun getAllAgentsFromRepository(): Resource<List<AgentData>> {
lateinit var agentData : Resource<List<AgentData>>

try {
agentData = localeDataSource.getAgentDataFromLocal()
} catch (e: Exception) {
Log.e(TAG, e.message.toString())
}

if (!agentData.data.isNullOrEmpty()) {
return agentData
} else {
agentData = responseToResource(remoteDataSource.getAgentFromRemote())
localeDataSource.saveAgentDataToLocal(agentData.data!!.filter {
it.agentIsPlayableCharacter
})
}

return agentData
}

private fun responseToResource(response : Response<Agent>) : Resource<List<AgentData>> {
if (response.isSuccessful) {
response.body()?.let {
return Resource.Success(
it.agents.filter { agent ->
agent.agentIsPlayableCharacter
}
)
}
}
return Resource.Error(response.message())
}

companion object {
private val TAG = AgentRepositoryImpl::class.java.toString()
}
}

UseCase

class GetAgentUseCase @Inject constructor(
private val agentRepository: AgentRepository
) {

suspend fun execute() : Resource<List<AgentData>> {
return agentRepository.getAllAgentsFromRepository()
}
}

ViewModel

@HiltViewModel
class AgentViewModel @Inject constructor(
private val app : Application,
private val getAgentUseCase: GetAgentUseCase
) : BaseViewModel(app) {

private var agent = MutableLiveData<Resource<List<AgentData>>>()
val agentData : LiveData<Resource<List<AgentData>>>
get() = agent

fun getAllAgentData() = viewModelScope.launch(Dispatchers.IO) {
agent.postValue(Resource.Loading())

try {
val result = getAgentUseCase.execute()
agent.postValue(result)
} catch (e : Exception) {
agent.postValue(Resource.Error(e.message.toString()))
}
}

}

Fragment

@AndroidEntryPoint
class AgentFragment : Fragment() {

private var _binding : FragmentAgentBinding? = null
private val binding get() = _binding!!

private val viewModel by lazy {
ViewModelProvider(this, factory = factory).get(AgentViewModel::class.java)
}

@Inject
lateinit var factory: AgentViewModelFactory

@Inject
lateinit var agentAdapter: AgentAdapter

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAgentBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewPagerAgent.apply {
adapter = agentAdapter
}

setAgentData()

binding.lottieAnimation.apply {
visibility = View.VISIBLE
playAnimation()
}
}

private fun setAgentData() {
viewModel.getAllAgentData()
viewModel.agentData.observe(viewLifecycleOwner) { response ->
when(response) {
is Resource.Success -> {
setProgressBar(false)
response.data.let {
agentAdapter.setData(it!!)
}
}
is Resource.Error -> {
setProgressBar(false)
response.message?.let {
Log.e("AgentFragment", it)
}
}
is Resource.Loading -> {
setProgressBar(true)
}
}
}
}

private fun setProgressBar(isShown : Boolean) {
binding.progressBar.visibility = if (isShown) View.VISIBLE else View.GONE
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

Navigate Maps Like a Pro

Success in Valorant goes beyond individual prowess — it requires a team effort and strategic positioning. Our app equips you with detailed map guides, offering insights into callouts, bomb plant locations, and choke points. From Bind to Split, master the layouts and dominate every round with superior map knowledge.

API Service

    @GET("v1/maps")
suspend fun getMapRemote() : Response<Map>

Room Database


@Database(
entities = [AgentData::class, MapData::class, WeaponData::class],
version = 3,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class ValorantDatabase : RoomDatabase() {

abstract fun agentDao() : AgentDao

abstract fun mapDao() : MapDao

abstract fun weaponDao() : WeaponDao
}
@Dao
interface MapDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveMapDatabase(listMapData : List<MapData>)

@Query("SELECT * FROM map_data")
fun getMapListDatabase() : List<MapData>
}

Repository

interface MapRepository {

suspend fun getAllMapsFromRepository() : Resource<List<MapData>>
}
class MapRepositoryImpl @Inject constructor(
private val mapRemoteDataSource: MapRemoteDataSource,
private val mapLocalDataSource: MapLocalDataSource
) : MapRepository {

override suspend fun getAllMapsFromRepository(): Resource<List<MapData>> {
lateinit var listMapData : Resource<List<MapData>>

try {
listMapData = mapLocalDataSource.getMapDataFromLocal()
} catch (e : Exception) {
Log.e("MapRepositoryImpl", e.message.toString())
}

if (listMapData.data!!.isNotEmpty()) {
return listMapData
} else {
listMapData = responseToResource(mapRemoteDataSource.getMapFromRemote())
mapLocalDataSource.saveMapDataToLocal(listMapData.data!!)
}

return listMapData
}

private fun responseToResource(response: Response<Map>) : Resource<List<MapData>>{
if (response.isSuccessful) {
response.body()?.let {
return Resource.Success(it.maps)
}
}
return Resource.Error(response.message())
}
}

ViewModel

@HiltViewModel
class MapViewModel @Inject constructor(
private val app: Application,
private val getMapUseCase: GetMapUseCase
) : BaseViewModel(app) {

private var map = MutableLiveData<Resource<List<MapData>>>()
val mapData : LiveData<Resource<List<MapData>>>
get() = map

fun getAllMapData() = viewModelScope.launch(Dispatchers.IO) {
map.postValue(Resource.Loading())

try {
val apiResult = getMapUseCase.execute()
map.postValue(apiResult)
} catch (e : Exception) {
map.postValue(Resource.Error(e.message.toString()))
}
}
}

Fragment

@AndroidEntryPoint
class MapFragment : Fragment() {

private var _binding : FragmentMapBinding? = null
private val binding get() = _binding!!

private val viewModel by lazy {
ViewModelProvider(this, factory).get(MapViewModel::class.java)
}

@Inject
lateinit var factory: MapViewModelFactory

@Inject
lateinit var mapAdapter: MapAdapter

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMapBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.recyclerViewMap.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = mapAdapter
}

mapAdapter.onMapItemClick = {
val fragment = MapDetailFragment.newInstance(
mapName = it.displayName,
mapCoordinate = it.coordinates ?: "",
mapSplashIcon = it.splash,
mapDisplayIcon = it.displayIcon
)

val fragmentManager = childFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()

fragmentTransaction.replace(R.id.frameLayoutMap, fragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
}

setMapData()
}

private fun setMapData() {
viewModel.getAllMapData()
viewModel.mapData.observe(viewLifecycleOwner) { response ->
when(response) {
is Resource.Success -> {
setProgressBar(false)
response.data.let {
mapAdapter.setData(it!!)
}
}
is Resource.Error -> {
setProgressBar(false)
response.message?.let {
Toast.makeText(activity, it, Toast.LENGTH_LONG).show()
println("An error occured: $it")
}
}
is Resource.Loading -> {
setProgressBar(true)
}
}
}
}

private fun setProgressBar(isShown : Boolean) {
binding.progressIndicator.visibility = if (isShown) View.VISIBLE else View.GONE
}

override fun onResume() {
super.onResume()
requireActivity().findViewById<BottomNavigationView>(R.id.bottomNavigationView).visibility = View.VISIBLE
}
}

Weapons Unleashed

An arsenal is only as effective as the one wielding it. The ‘Valorant Master Professional Guide’ app presents a comprehensive weapon encyclopedia, covering everything from pistols to rifles. Explore damage stats, recoil patterns, and tactical considerations to make informed decisions about your loadout. Stay ahead of the meta with regularly updated weapon information.

API Service

    @GET("v1/weapons")
suspend fun getWeaponRemote() : Response<Weapon>

Room Database

@Database(
entities = [AgentData::class, MapData::class, WeaponData::class],
version = 3,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class ValorantDatabase : RoomDatabase() {

abstract fun agentDao() : AgentDao

abstract fun mapDao() : MapDao

abstract fun weaponDao() : WeaponDao
}
@Dao
interface WeaponDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveWeaponDatabase(weaponList: List<WeaponData>)

@Query("SELECT * FROM weapon_data")
fun getWeaponListDatabase(): List<WeaponData>
}

Repository

interface WeaponRepository {

suspend fun getAllWeaponsFromRepository() : Resource<List<WeaponData>>
}
class WeaponRepositoryImpl @Inject constructor(
private val weaponRemoteDataSource: WeaponRemoteDataSource,
private val weaponLocalDataSource: WeaponLocalDataSource
) : WeaponRepository {

override suspend fun getAllWeaponsFromRepository(): Resource<List<WeaponData>> {
lateinit var listWeaponData : Resource<List<WeaponData>>

try {
listWeaponData = weaponLocalDataSource.getWeaponDataFromLocal()
} catch (e: Exception) {
Log.e(TAG, e.message.toString())
}

if (listWeaponData.data!!.isNotEmpty()) {
return listWeaponData
} else {
listWeaponData = responseToResource(weaponRemoteDataSource.getWeaponFromRemote())
weaponLocalDataSource.saveWeaponDataToLocal(listWeaponData.data!!)
}

return listWeaponData
}

private fun responseToResource(response : Response<Weapon>) : Resource<List<WeaponData>> {
if (response.isSuccessful) {
response.body()?.let {
return Resource.Success(it.weapons)
}
}
return Resource.Error(response.message())
}

companion object {
private val TAG = WeaponRepositoryImpl::class.java.toString()
}
}

UseCase

class GetWeaponUseCase @Inject constructor(
private val weaponRepository: WeaponRepository
){

suspend fun execute() : Resource<List<WeaponData>> {
return weaponRepository.getAllWeaponsFromRepository()
}
}

ViewModel

@HiltViewModel
class WeaponViewModel @Inject constructor(
app: Application,
private val getWeaponUseCase: GetWeaponUseCase
) : BaseViewModel(app) {

private var weaponList = MutableLiveData<Resource<List<WeaponData>>>()
val weaponListData : LiveData<Resource<List<WeaponData>>>
get() = weaponList

private var weaponDetail = MutableLiveData<Resource<WeaponData>>()
val weaponDetailData : LiveData<Resource<WeaponData>>
get() = weaponDetail


fun getAllWeaponData(weaponCategory : String) = viewModelScope.launch(Dispatchers.IO) {
weaponList.postValue(Resource.Loading())

try {
val apiResult = getWeaponUseCase.execute()
val newList : ArrayList<WeaponData> = arrayListOf()
for (i in apiResult.data!!.indices) {
if (apiResult.data[i].weaponCategory.split("::")[1] == weaponCategory) {
newList.add(apiResult.data[i])
}
}
weaponList.postValue(Resource.Success(newList))
} catch (e: Exception) {
weaponList.postValue(Resource.Error(e.message.toString()))
}
}

fun getDetailWeaponData(uuid : String) = viewModelScope.launch(Dispatchers.IO) {
weaponDetail.postValue(Resource.Loading())

try {
val result = getWeaponUseCase.execute()
result.data?.forEach {
if (it.uuid == uuid) {
weaponDetail.postValue(Resource.Success(it))
}
}
} catch (e: Exception) {
weaponDetail.postValue(Resource.Error(e.message.toString()))
}
}
}

Fragment

@AndroidEntryPoint
class WeaponFragment : Fragment() {

private var _binding: FragmentWeaponBinding? = null
private val binding get() = _binding!!

private val viewModel by lazy {
ViewModelProvider(this, factory).get(WeaponViewModel::class.java)
}

@Inject
lateinit var factory: WeaponViewModelFactory
@Inject
lateinit var weaponAdapter: WeaponAdapter

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWeaponBinding.inflate(inflater, container, false)
// java.lang.IllegalStateException: Cannot change whether this adapter has stable IDs while the adapter has registered observers.
if (!weaponAdapter.hasObservers()) {
weaponAdapter.setHasStableIds(true)
}
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
// Heavy Weapon
setWeaponDataUI("Heavy", linearLayoutHeavyTitle, recyclerViewHeavy, expandableLayoutHeavy)
// Rifle Weapon
setWeaponDataUI("Rifle", linearLayoutRifleTitle, recyclerViewRifle, expandableLayoutRifle)
// Sniper Weapon
setWeaponDataUI("Sniper", linearLayoutSniperTitle, recyclerViewSniper, expandableLayoutSniper)
// Shotgun Weapon
setWeaponDataUI("Shotgun", linearLayoutShotgunTitle, recyclerViewShotgun, expandableLayoutShotgun)
// SMG Weapon
setWeaponDataUI("SMG", linearLayoutSMGTitle, recyclerViewSMG, expandableLayoutSMG)
// SideArm Weapon
setWeaponDataUI("Sidearm", linearLayoutSidearmTitle, recyclerViewSidearm, expandableLayoutSidearm)
// Melee Weapon
setWeaponDataUI("Melee", linearLayoutMeleeTitle, recyclerViewMelee, expandableLayoutMelee)
}

weaponAdapter.onWeaponItemClick = {
val fragment = WeaponDetailFragment.newInstance(
weaponUUID = it
)
val fragmentManager = childFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()

fragmentTransaction.replace(R.id.frameLayoutWeapon, fragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
}
}
private fun setWeaponDataUI(
weaponCategory: String,
linearLayoutTitle: LinearLayout,
recyclerView: RecyclerView,
expandableLayout: ExpandableLayout
) {
expandableLayout.setInterpolator(LinearInterpolator())

linearLayoutTitle.setOnClickListener {
viewModel.getAllWeaponData(weaponCategory)
viewModel.weaponListData.observe(viewLifecycleOwner, ::setLiveData)
Handler(Looper.getMainLooper()).postDelayed({
if (!expandableLayout.isExpanded) {
closeAllExpandableLayout()
expandableLayout.isExpanded = true
} else {
expandableLayout.isExpanded = false
weaponAdapter.setData(listOf())
}
}, 200)
}
recyclerView.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = weaponAdapter
}
}

private fun setLiveData(response: Resource<List<WeaponData>>) {
when (response) {
is Resource.Success -> {
setProgressBar(false)
response.data.let { listWeaponData ->
listWeaponData?.let {
weaponAdapter.setData(it)
//Log.i("Weapon Success Data", it.toString())
}

}
}
is Resource.Error -> {
setProgressBar(false)
response.message?.let { errorMessage ->
Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG).show()
Log.e("Weapon Error Data", errorMessage)
}
}
is Resource.Loading -> {
setProgressBar(true)
}
}
}

private fun closeAllExpandableLayout() {
binding.apply {
expandableLayoutHeavy.isExpanded = false
expandableLayoutRifle.isExpanded = false
expandableLayoutSniper.isExpanded = false
expandableLayoutShotgun.isExpanded = false
expandableLayoutSMG.isExpanded = false
expandableLayoutSidearm.isExpanded = false
expandableLayoutMelee.isExpanded = false
}
}

private fun setProgressBar(isShown : Boolean) {
binding.progressIndicator.visibility = if (isShown) View.VISIBLE else View.GONE
}

override fun onResume() {
super.onResume()
requireActivity().findViewById<BottomNavigationView>(R.id.bottomNavigationView)?.visibility = View.VISIBLE
}
}

İbrahim Can Erdoğan

LINKEDIN

YOUTUBE

UDEMY

GITHUB

--

--

ibrahimcanerdogan
ibrahimcanerdogan

Written by ibrahimcanerdogan

Hi, My name is Ibrahim, I am developing ebebek android app within Ebebek. I publish various articles in the field of programming and self-improvement.

No responses yet