Add rest of the implementation
This commit is contained in:
parent
b6a511165b
commit
5b890d495f
22 changed files with 454 additions and 0 deletions
|
@ -4,6 +4,7 @@ plugins {
|
|||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.dagger.hilt.android")
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -63,6 +64,9 @@ dependencies {
|
|||
implementation(libs.coil.kt.compose)
|
||||
implementation(libs.retrofit2.retrofit)
|
||||
implementation(libs.retrofit.retrofit2.kotlinx.serialization.converter)
|
||||
implementation(libs.hilt.android)
|
||||
implementation (libs.okhttp)
|
||||
kapt(libs.hilt.android.compiler)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
|
|
|
@ -4,18 +4,29 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.ui.Modifier
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import xyz.leomurca.rickandmorty.data.CharacterRepository
|
||||
import xyz.leomurca.rickandmorty.ui.home.HomeScreen
|
||||
import xyz.leomurca.rickandmorty.ui.home.HomeViewModel
|
||||
import xyz.leomurca.rickandmorty.ui.theme.RickAndMortyTheme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: HomeViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
RickAndMortyTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
HomeScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package xyz.leomurca.rickandmorty
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application()
|
|
@ -0,0 +1,7 @@
|
|||
package xyz.leomurca.rickandmorty.data
|
||||
|
||||
import xyz.leomurca.rickandmorty.data.model.Character
|
||||
|
||||
interface CharacterRepository {
|
||||
suspend fun characters(): CharacterResult<List<Character>>
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package xyz.leomurca.rickandmorty.data
|
||||
|
||||
sealed class CharacterResult<out T> {
|
||||
data class Success<T>(val data: T) : CharacterResult<T>()
|
||||
data class Error(val message: String) : CharacterResult<Nothing>()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package xyz.leomurca.rickandmorty.data.model
|
||||
|
||||
data class Character(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val image: String
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
package xyz.leomurca.rickandmorty.data.remote
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.leomurca.rickandmorty.data.CharacterRepository
|
||||
import xyz.leomurca.rickandmorty.data.CharacterResult
|
||||
import xyz.leomurca.rickandmorty.data.model.Character
|
||||
import xyz.leomurca.rickandmorty.di.AppDispatchers
|
||||
import xyz.leomurca.rickandmorty.di.Dispatcher
|
||||
import xyz.leomurca.rickandmorty.network.NetworkDataSource
|
||||
import xyz.leomurca.rickandmorty.network.NetworkResult
|
||||
|
||||
class RemoteCharacterRepository(
|
||||
@Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
private val dataSource: NetworkDataSource,
|
||||
) : CharacterRepository {
|
||||
override suspend fun characters(): CharacterResult<List<Character>> {
|
||||
return withContext(ioDispatcher) {
|
||||
when (val result = dataSource.characters()) {
|
||||
is NetworkResult.Success -> {
|
||||
CharacterResult.Success(
|
||||
result.data.results.map {
|
||||
Character(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
image = it.image
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkResult.Error -> {
|
||||
CharacterResult.Error(result.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
app/src/main/java/xyz/leomurca/rickandmorty/di/DataModule.kt
Normal file
25
app/src/main/java/xyz/leomurca/rickandmorty/di/DataModule.kt
Normal file
|
@ -0,0 +1,25 @@
|
|||
package xyz.leomurca.rickandmorty.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import xyz.leomurca.rickandmorty.data.CharacterRepository
|
||||
import xyz.leomurca.rickandmorty.data.remote.RemoteCharacterRepository
|
||||
import xyz.leomurca.rickandmorty.network.NetworkDataSource
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMovieRepository(
|
||||
@Dispatcher(AppDispatchers.IO) ioDispatcher: CoroutineDispatcher,
|
||||
dataSource: NetworkDataSource,
|
||||
): CharacterRepository {
|
||||
return RemoteCharacterRepository(ioDispatcher, dataSource)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package xyz.leomurca.rickandmorty.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Dispatcher(val appDispatcher: AppDispatchers)
|
||||
|
||||
enum class AppDispatchers {
|
||||
Default,
|
||||
IO,
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DispatchersModule {
|
||||
@Provides
|
||||
@Dispatcher(AppDispatchers.IO)
|
||||
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@Provides
|
||||
@Dispatcher(AppDispatchers.Default)
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package xyz.leomurca.rickandmorty.di
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import xyz.leomurca.rickandmorty.BuildConfig
|
||||
import xyz.leomurca.rickandmorty.network.NetworkDataSource
|
||||
import xyz.leomurca.rickandmorty.network.remote.ApiService
|
||||
import xyz.leomurca.rickandmorty.network.remote.RemoteNetworkDataSource
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesNetworkJson(): Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkDataSource(apiService: ApiService, json: Json): NetworkDataSource {
|
||||
return RemoteNetworkDataSource(apiService, json)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(json: Json): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiService(retrofit: Retrofit): ApiService {
|
||||
return retrofit.create(ApiService::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package xyz.leomurca.rickandmorty.network
|
||||
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacter
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacterResponse
|
||||
|
||||
interface NetworkDataSource {
|
||||
suspend fun characters(): NetworkResult<NetworkCharacterResponse>
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package xyz.leomurca.rickandmorty.network
|
||||
|
||||
sealed class NetworkResult<out T> {
|
||||
data class Success<T>(val data: T) : NetworkResult<T>()
|
||||
data class Error(val errorMessage: String) : NetworkResult<Nothing>()
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package xyz.leomurca.rickandmorty.network.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NetworkCharacter(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val image: String
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package xyz.leomurca.rickandmorty.network.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NetworkCharacterResponse(
|
||||
val results: List<NetworkCharacter>,
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package xyz.leomurca.rickandmorty.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NetworkError(
|
||||
@SerialName("error") val message: String
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package xyz.leomurca.rickandmorty.network.remote
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacter
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacterResponse
|
||||
|
||||
interface ApiService {
|
||||
|
||||
@GET("character")
|
||||
suspend fun characters(): Response<NetworkCharacterResponse>
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package xyz.leomurca.rickandmorty.network.remote
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.leomurca.rickandmorty.network.NetworkDataSource
|
||||
import xyz.leomurca.rickandmorty.network.NetworkResult
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacter
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkCharacterResponse
|
||||
import xyz.leomurca.rickandmorty.network.model.NetworkError
|
||||
|
||||
class RemoteNetworkDataSource(
|
||||
private val apiService: ApiService,
|
||||
private val json: Json
|
||||
): NetworkDataSource {
|
||||
override suspend fun characters(): NetworkResult<NetworkCharacterResponse> {
|
||||
return try {
|
||||
val response = apiService.characters()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
NetworkResult.Success(response.body() ?: throw Exception("Empty response body"))
|
||||
} else {
|
||||
val errorBody = response.errorBody()?.string() ?: ""
|
||||
val networkError = json.decodeFromString<NetworkError>(errorBody)
|
||||
NetworkResult.Error(networkError.message)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
NetworkResult.Error(e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package xyz.leomurca.rickandmorty.ui.home
|
||||
|
||||
import android.util.Log
|
||||
import xyz.leomurca.rickandmorty.data.model.Character as Character
|
||||
import android.widget.Space
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardElevation
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(viewModel: HomeViewModel) {
|
||||
val state = viewModel.uiState.collectAsState()
|
||||
|
||||
when (val value = state.value) {
|
||||
is HomeViewModel.UiState.Loading -> CharacterCardSkeleton()
|
||||
is HomeViewModel.UiState.Loaded.Success -> CharacterStaggeredGrid(value.characters)
|
||||
is HomeViewModel.UiState.Loaded.Error -> Text(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CharacterStaggeredGrid(characters: List<Character>) {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Adaptive(minSize = 150.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(characters.size) { index ->
|
||||
CharacterCard(character = characters[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CharacterCard(character: Character) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(character.image)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = character.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = character.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CharacterCardSkeleton() {
|
||||
val shimmerColors = listOf(
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
Color.LightGray.copy(alpha = 0.2f),
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
)
|
||||
|
||||
val transition = rememberInfiniteTransition()
|
||||
val translateAnim = transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1000f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val brush = Brush.linearGradient(
|
||||
colors = shimmerColors,
|
||||
start = Offset.Zero,
|
||||
end = Offset(x = translateAnim.value, y = translateAnim.value)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.background(brush = brush, shape = RectangleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(80.dp)
|
||||
.height(16.dp)
|
||||
.background(brush = brush, shape = RectangleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package xyz.leomurca.rickandmorty.ui.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.leomurca.rickandmorty.data.CharacterRepository
|
||||
import xyz.leomurca.rickandmorty.data.CharacterResult
|
||||
import xyz.leomurca.rickandmorty.data.model.Character
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val characterRepository: CharacterRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = when (val result = characterRepository.characters()) {
|
||||
is CharacterResult.Success -> UiState.Loaded.Success(result.data)
|
||||
is CharacterResult.Error -> UiState.Loaded.Error(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface UiState {
|
||||
data object Loading : UiState
|
||||
|
||||
sealed class Loaded : UiState {
|
||||
data class Success(val characters: List<Character>) : Loaded()
|
||||
data class Error(val message: String) : Loaded()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,4 +4,5 @@ plugins {
|
|||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
[versions]
|
||||
agp = "8.8.2"
|
||||
hiltAndroid = "2.51.1"
|
||||
kotlin = "2.0.20"
|
||||
coreKtx = "1.15.0"
|
||||
junit = "4.13.2"
|
||||
|
@ -11,11 +12,14 @@ composeBom = "2025.03.00"
|
|||
navigation = "2.8.9"
|
||||
kotlinxSerializationJson = "1.8.0"
|
||||
coil = "3.1.0"
|
||||
okhttp = "4.12.0"
|
||||
retrofit = "1.0.0"
|
||||
retrofit2 = "2.11.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
|
@ -33,6 +37,7 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio
|
|||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
coil-kt = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" }
|
||||
coil-kt-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" }
|
||||
retrofit-retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofit" }
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue