Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android {
"proguard-rules.pro"
)
buildConfigField("String", "API_KEY", "\"$coinApiKey\"")
signingConfig = signingConfigs.getByName("debug")

}
debug {
Expand Down Expand Up @@ -110,6 +111,11 @@ dependencies {
//Log
implementation(libs.timber)

debugImplementation(libs.flipper)
debugImplementation(libs.flipper.network)
debugImplementation(libs.flipper.leakcanary)
debugImplementation(libs.soloader)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.velosobr.cryptoexchangesapp

import android.content.Context
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.soloader.SoLoader
import org.koin.core.context.GlobalContext.get

object FlipperInitializer {
fun init(context: Context) {
SoLoader.init(context, false)
val client = AndroidFlipperClient.getInstance(context)
val networkPlugin = get().get<NetworkFlipperPlugin>()
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
client.addPlugin(networkPlugin)
client.start()
}
}
7 changes: 4 additions & 3 deletions app/src/main/java/com/velosobr/cryptoexchangesapp/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ class App : Application() {
override fun onCreate() {
super.onCreate()

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
startKoin {
androidContext(this@App)
modules(
Expand All @@ -27,6 +24,10 @@ class App : Application() {
)
)
}
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
FlipperInitializer.init(this)
}
}

}
3 changes: 2 additions & 1 deletion data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ dependencies {
implementation(libs.koin.core)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)

debugImplementation(libs.flipper)
debugImplementation(libs.flipper.network)
ksp(libs.moshi.codegen)
testImplementation(libs.junit)
testImplementation(libs.mockk)
Expand Down
14 changes: 12 additions & 2 deletions data/src/main/java/com/velosobr/data/di/DataModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.velosobr.data.di

import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.velosobr.cryptoexchangesapp.data.BuildConfig
import com.velosobr.data.remote.api.ExchangeApiService
import com.velosobr.data.remote.interceptor.LoggingInterceptor
Expand All @@ -10,18 +12,26 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

val dataModule = module {
factory {

single<NetworkFlipperPlugin> {
check(BuildConfig.DEBUG) { "NetworkFlipperPlugin só deve ser injetado no modo DEBUG" }
NetworkFlipperPlugin()
}

single {
OkHttpClient.Builder().apply {
if (BuildConfig.DEBUG) {
addInterceptor(LoggingInterceptor())
addNetworkInterceptor(FlipperOkhttpInterceptor(get()))
}
}.build()
}

single {
Retrofit.Builder()
.baseUrl("https://rest.coinapi.io/")
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient.Builder().build())
.client(get())
.build()
.create(ExchangeApiService::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,4 @@ class ExchangeIconDataStore(private val context: Context) {
.map { prefs -> prefs[iconKey(exchangeId)] }
.first()


suspend fun clearIcon(exchangeId: String) {
context.dataStore.edit { prefs ->
prefs.remove(iconKey(exchangeId))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class LoggingInterceptor : Interceptor {
val durationMs = (endTime - startTime) / 1e6

Timber.tag("Network")
.d("→ ${request.method()} ${request.url()} | ← ${response.code()} (${durationMs}ms)")
.d("→ ${request.method} ${request.url} | ← ${response.code} (${durationMs}ms)")

return response
}
Expand Down
2 changes: 1 addition & 1 deletion feature/exchange_list/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ dependencies {
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)

implementation(libs.accompanist.swiperefresh)
debugImplementation(libs.androidx.ui.tooling)

implementation(libs.androidx.lifecycle.viewmodel.ktx)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.velosobr.exchange_list.presentation

import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
Expand All @@ -15,6 +16,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.velosobr.core.state.UiState
import com.velosobr.cryptoexchangesapp.data.BuildConfig
import com.velosobr.designsystem.components.DSExchangeCardComponent
Expand All @@ -34,78 +37,99 @@ fun ExchangeListScreen(

val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val iconMap by viewModel.exchangeIcons.collectAsStateWithLifecycle()
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()

ExchangeListContent(
uiState = uiState,
iconMap = iconMap,
onExchangeClick = onExchangeClick,
onRetryClick = {
viewModel.fetchExchanges()
})
},
isRefreshing = isRefreshing,
onRefresh = {
viewModel.refreshExchanges()
}
)
}

@Composable
fun ExchangeListContent(
uiState: UiState<List<Exchange>>,
iconMap: Map<String, String?>,
onExchangeClick: (String, String) -> Unit,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onRetryClick: () -> Unit
) {
Scaffold(topBar = {
Text(
text = "Exchanges List",
style = DSAppTypography.titleLarge,
color = DSColor.DarkText,
modifier = Modifier
.padding(horizontal = DSSpacing.md, vertical = DSSpacing.lg)
)
}) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.background(DSColor.DarkBackground)
.fillMaxSize()

val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)

Scaffold(
topBar = {
Text(
text = "Exchanges List",
style = DSAppTypography.titleLarge,
color = DSColor.DarkText,
modifier = Modifier
.padding(horizontal = DSSpacing.md, vertical = DSSpacing.lg)
)
}
) { paddingValues ->
SwipeRefresh(
state = swipeRefreshState,
onRefresh = onRefresh,
indicatorPadding = paddingValues,
modifier = Modifier.fillMaxSize()
) {
when (uiState) {
is UiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
Box(
modifier = Modifier
.padding(paddingValues)
.background(DSColor.DarkBackground)
.fillMaxSize()
) {
when (uiState) {
is UiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}

is UiState.Success -> {
val exchanges = uiState.data
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = DSSpacing.xxxl)
) {
items(items = exchanges, key = { it.exchangeId }) { exchange ->
val iconUrl = iconMap[exchange.exchangeId] ?: BuildConfig.DEFAULT_ICON_URL
val model = exchange.toCardModel()
is UiState.Success -> {
val exchanges = uiState.data
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = DSSpacing.xxxl)
) {
items(items = exchanges, key = { it.exchangeId }) { exchange ->
val iconUrl =
iconMap[exchange.exchangeId] ?: BuildConfig.DEFAULT_ICON_URL
val model = exchange.toCardModel()


DSExchangeCardComponent(
name = model.name,
id = model.id,
volume = model.volume,
iconUrl = iconUrl,
onClick = { onExchangeClick(model.id, iconUrl) },
modifier = Modifier
.padding(horizontal = DSSpacing.md)
.animateItem(
fadeInSpec = androidx.compose.animation.core.tween(300),
fadeOutSpec = androidx.compose.animation.core.tween(300),
)
)
DSExchangeCardComponent(
name = model.name,
id = model.id,
volume = model.volume,
iconUrl = iconUrl,
onClick = { onExchangeClick(model.id, iconUrl) },
modifier = Modifier
.padding(horizontal = DSSpacing.md)
.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
)
)

}
}
}
}

is UiState.Error -> {
ErrorBox(
title = "Oops! Something went wrong.",
message = uiState.message,
onRetry = onRetryClick
)
is UiState.Error -> {
ErrorBox(
title = "Oops! Something went wrong.",
message = uiState.message,
onRetry = onRetryClick
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.velosobr.core.state.UiState
import com.velosobr.domain.model.Exchange
import com.velosobr.domain.usecase.GetExchangeIconUrlUseCase
import com.velosobr.domain.usecase.GetExchangesUseCase
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -22,6 +23,8 @@ class ExchangeListViewModel(
val uiState: StateFlow<UiState<List<Exchange>>> = _uiState.asStateFlow()
private val _exchangeIcons = MutableStateFlow<Map<String, String?>>(emptyMap())
val exchangeIcons: StateFlow<Map<String, String?>> = _exchangeIcons
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> = _isRefreshing

init {
fetchExchanges()
Expand Down Expand Up @@ -51,4 +54,13 @@ class ExchangeListViewModel(
}
}
}

fun refreshExchanges() {
viewModelScope.launch {
_isRefreshing.value = true
fetchExchanges()
delay(500)
_isRefreshing.value = false
}
}
}
10 changes: 7 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ datastore = "1.0.0"
kotlinx-serialization = "1.6.0"
accompanist = "0.34.0"
turbine = "1.1.0"

flipper = "0.234.0"
Comment thread
velosobr marked this conversation as resolved.
soloader = "0.10.5"

[libraries]
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.6.2" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
Expand Down Expand Up @@ -76,13 +78,15 @@ moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" }
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
moshi-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

flipper = { module = "com.facebook.flipper:flipper", version.ref = "flipper" }
flipper-network = { module = "com.facebook.flipper:flipper-network-plugin", version.ref = "flipper" }
flipper-leakcanary = { module = "com.facebook.flipper:flipper-leakcanary2-plugin", version.ref = "flipper" }
soloader = { module = "com.facebook.soloader:soloader", version.ref = "soloader" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.28" }