Skip to content

Commit

Permalink
Merge pull request #4 from alexandrucaraus/refined_package_structure
Browse files Browse the repository at this point in the history
refined_package_structure
  • Loading branch information
alexandrucaraus authored Sep 4, 2024
2 parents 7d049b4 + 300029e commit b801488
Show file tree
Hide file tree
Showing 37 changed files with 129 additions and 168 deletions.
3 changes: 0 additions & 3 deletions features/news/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ dependencies {
testFixturesImplementation(libs.androidx.compose.ui)
}


android {
namespace = "eu.acaraus.news"
compileSdk = libs.versions.compileSdk.get().toInt()
Expand Down Expand Up @@ -126,5 +125,3 @@ android {
enable = true
}
}


Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package eu.acaraus.news.data

import eu.acaraus.news.domain.repositories.LocaleStore
import eu.acaraus.news.domain.repositories.LocaleRepository
import org.koin.core.annotation.Single
import java.util.Locale


@Single(binds = [LocaleStore::class])
class LocaleStoreImpl : LocaleStore {
@Single(binds = [LocaleRepository::class])
class Locale : LocaleRepository {

private var languageCode = Locale.getDefault().language
private var countryCode = Locale.getDefault().country
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import android.os.Bundle
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import eu.acaraus.news.domain.repositories.SpeechEvent
import eu.acaraus.news.domain.repositories.SpeechRecognizer
import eu.acaraus.news.domain.repositories.SpeechRecognizerService
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import org.koin.core.annotation.Factory
import android.speech.SpeechRecognizer as AndroidSpeechRecognizer

@Factory(binds = [SpeechRecognizer::class])
class SpeechRecognizerImpl(
@Factory(binds = [SpeechRecognizerService::class])
class SpeechRecognizer(
context: Context,
) : SpeechRecognizer {
) : SpeechRecognizerService {

override val isAvailable = MutableStateFlow(
AndroidSpeechRecognizer.isRecognitionAvailable(context),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import eu.acaraus.news.domain.entities.ArticlesFilter
import eu.acaraus.news.domain.entities.ArticlesSources
import eu.acaraus.news.domain.entities.NewsError
import eu.acaraus.news.domain.entities.SortBy
import eu.acaraus.news.domain.repositories.NewsApi
import eu.acaraus.news.domain.repositories.NewsRepository
import eu.acaraus.shared.lib.Either
import eu.acaraus.shared.lib.Either.Success
import eu.acaraus.shared.lib.coroutines.DispatcherProvider
Expand All @@ -19,19 +19,18 @@ import kotlinx.coroutines.withContext
import org.koin.core.annotation.Factory
import java.time.format.DateTimeFormatter

@Factory(binds = [NewsApi::class])
class NewsApiImpl(
@Factory(binds = [NewsRepository::class])
class NewsApi(
private val httpClient: HttpClient,
private val httpClientConfig: HttpClientConfig,
private val dispatchers: DispatcherProvider,
) : NewsApi {
) : NewsRepository {

override suspend fun getHeadlines(
language: String,
category: String,
): Either<List<Article>, NewsError> = withContext(dispatchers.io) {
kotlin.runCatching {

val response = httpClient.get(path("/v2/top-headlines")) {
parameter(LANGUAGE, language)
parameter(CATEGORY, category)
Expand All @@ -43,7 +42,6 @@ class NewsApiImpl(

else -> Either.Error(response.toError())
}

}.getOrElse { failure -> Either.error(failure.toError()) }
}

Expand All @@ -62,20 +60,18 @@ class NewsApiImpl(

else -> Either.Error(response.toError())
}

}.getOrElse { failure -> Either.error(failure.toError()) }
}

override suspend fun getEverything(filter: ArticlesFilter): Either<List<Article>, NewsError> =
withContext(dispatchers.io) {
kotlin.runCatching {

val response: NewsApiResponse = httpClient.get((path("/v2/everything"))) {
parameter(LANGUAGE, filter.language)
parameter(
filter.query.isNotBlank(),
QUERY,
filter.query
filter.query,
)
parameter(
filter.sources.isNotEmpty(),
Expand All @@ -93,17 +89,15 @@ class NewsApiImpl(

else -> Either.Error(response.toError())
}

}.getOrElse { failure -> Either.error(failure.toError()) }

}

private fun path(path: String) = httpClientConfig.baseUrl + path

private fun HttpRequestBuilder.parameter(
condition: Boolean,
key: String,
value: Any?
value: Any?,
) {
if (condition) parameter(key, value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import org.koin.core.annotation.Single
@Single
fun client(
json: Json,
httpClientConfig: HttpClientConfig
httpClientConfig: HttpClientConfig,
): HttpClient =
HttpClient(OkHttp) {
install(DefaultRequest) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package eu.acaraus.news.domain.repositories

interface LocaleStore {
interface LocaleRepository {
fun setLanguageCode(code: String)
fun getLanguageCode(): String
fun setCountryCode(code: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import eu.acaraus.news.domain.entities.ArticlesSources
import eu.acaraus.news.domain.entities.NewsError
import eu.acaraus.shared.lib.Either

interface NewsApi {
interface NewsRepository {

suspend fun getHeadlines(
language: String = "de",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sealed class SpeechEvent {
data class Result(val matches: List<String>) : SpeechEvent()
}

interface SpeechRecognizer {
interface SpeechRecognizerService {
val isListening: MutableStateFlow<Boolean>
val isAvailable: MutableStateFlow<Boolean>
fun events(): Flow<SpeechEvent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import eu.acaraus.news.domain.entities.Article
import eu.acaraus.news.domain.entities.ArticlesFilter
import eu.acaraus.news.domain.entities.NewsError
import eu.acaraus.news.domain.entities.toNewsError
import eu.acaraus.news.domain.repositories.LocaleStore
import eu.acaraus.news.domain.repositories.NewsApi
import eu.acaraus.news.domain.repositories.LocaleRepository
import eu.acaraus.news.domain.repositories.NewsRepository
import eu.acaraus.shared.lib.Either
import eu.acaraus.shared.lib.map
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -20,8 +20,8 @@ import java.util.Locale

@Factory
class GetArticles(
private val newsApi: NewsApi,
private val localeStore: LocaleStore,
private val newsRepository: NewsRepository,
private val localeRepository: LocaleRepository,
) {

operator fun invoke(filter: ArticlesFilter): Flow<Either<List<Article>, NewsError>> =
Expand All @@ -38,11 +38,11 @@ class GetArticles(
private fun ArticlesFilter.isDefault() = this == ArticlesFilter()

private fun fetchHeadlines(): Flow<Either<List<Article>, NewsError>> = flow {
emit(newsApi.getHeadlines(language = localeStore.getLanguageCode()))
emit(newsRepository.getHeadlines(language = localeRepository.getLanguageCode()))
}

private fun fetchEverything(filter: ArticlesFilter): Flow<Either<List<Article>, NewsError>> = flow {
emit(newsApi.getEverything(filter))
emit(newsRepository.getEverything(filter))
}

private fun Flow<Either<List<Article>, NewsError>>.filterRemovedArticles() = map { result ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package eu.acaraus.news.domain.usecases
import eu.acaraus.news.domain.entities.ArticlesSources
import eu.acaraus.news.domain.entities.NewsError
import eu.acaraus.news.domain.entities.toNewsError
import eu.acaraus.news.domain.repositories.LocaleStore
import eu.acaraus.news.domain.repositories.NewsApi
import eu.acaraus.news.domain.repositories.LocaleRepository
import eu.acaraus.news.domain.repositories.NewsRepository
import eu.acaraus.shared.lib.Either
import eu.acaraus.shared.lib.map
import eu.acaraus.shared.lib.onEachSuccess
Expand All @@ -17,21 +17,21 @@ import java.util.concurrent.atomic.AtomicReference

@Factory
class GetArticlesSources(
private val newsApi: NewsApi,
private val localeStore: LocaleStore,
private val newsRepository: NewsRepository,
private val localeRepository: LocaleRepository,
) {

private val atomicArticleSources = AtomicReference<List<ArticlesSources>>(emptyList())

operator fun invoke(): Flow<Either<List<ArticlesSources>, NewsError>> = flow {
if (atomicArticleSources.get().isEmpty()) {
newsApi
newsRepository
.getSources()
.onEachSuccess { atomicArticleSources.set(it) }
}
emit(Either.success<List<ArticlesSources>, NewsError>(atomicArticleSources.get()))
}.map { sourcesResult ->
sourcesResult.map { sources -> sources.filter { it.language == localeStore.getLanguageCode() } }
sourcesResult.map { sources -> sources.filter { it.language == localeRepository.getLanguageCode() } }
}.catch { cause ->
emit(Either.error(cause.toNewsError()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.acaraus.design.components.Toaster
import eu.acaraus.design.components.ToasterState
import eu.acaraus.news.domain.entities.Article
import eu.acaraus.news.presentation.list.components.ArticleFilter
import eu.acaraus.news.presentation.list.components.ArticleFilterActionIcon
Expand All @@ -16,8 +18,6 @@ import eu.acaraus.news.presentation.list.components.ArticleListState
import eu.acaraus.news.presentation.list.components.ArticlesFilterEditorState
import eu.acaraus.news.presentation.list.components.AudioCommandButton
import eu.acaraus.news.presentation.list.components.AudioCommandButtonState
import eu.acaraus.design.components.Toaster
import eu.acaraus.design.components.ToasterState

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import eu.acaraus.design.components.ToasterState
import eu.acaraus.news.domain.entities.Article
import eu.acaraus.news.presentation.list.components.ArticleListState
import eu.acaraus.news.presentation.list.components.ArticlesFilterEditorState
import eu.acaraus.news.presentation.list.components.AudioCommandButtonState
import eu.acaraus.design.components.ToasterState

@Composable
@Preview(showBackground = true, showSystemUi = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview


@Composable
@Preview(showBackground = true)
fun AudioCommandButtonPreview() = Column {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ class ArticlesListStateHolder(
updateError(
NewsError(
code = "unknown",
exception.message ?: "Failed to load articles"
)
exception.message ?: "Failed to load articles",
),
)
}
.launchIn(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package eu.acaraus.news.presentation.list.holders

import eu.acaraus.design.components.PermissionState
import eu.acaraus.design.components.ToasterState
import eu.acaraus.news.domain.repositories.SpeechEvent
import eu.acaraus.news.domain.repositories.SpeechRecognizer
import eu.acaraus.news.domain.repositories.SpeechRecognizerService
import eu.acaraus.news.domain.usecases.MatchVoiceCommands
import eu.acaraus.news.domain.usecases.VoiceCommand
import eu.acaraus.news.presentation.list.components.AudioCommandButtonState
import eu.acaraus.design.components.PermissionState
import eu.acaraus.design.components.ToasterState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -21,14 +21,14 @@ import org.koin.core.annotation.Scope
@Factory
@Scope(ArticlesListKoinScope::class)
class SpeechRecognizerStateHolder(
private val speechRecognizer: SpeechRecognizer,
private val speechRecognizerService: SpeechRecognizerService,
private val matchVoiceCommands: MatchVoiceCommands,
scope: CoroutineScope,
) : CoroutineScope by scope {

private val audioCommandButtonState = MutableStateFlow(
AudioCommandButtonState(
isEnabled = speechRecognizer.isAvailable.value,
isEnabled = speechRecognizerService.isAvailable.value,
toggleListening = ::toggleListening,
changePermissionState = ::changePermissionsState,
),
Expand All @@ -44,17 +44,17 @@ class SpeechRecognizerStateHolder(
val reloadCommand = MutableSharedFlow<Unit>()

init {
speechRecognizer.isListening.onEach(::updateListeningState).launchIn(this)
speechRecognizerService.isListening.onEach(::updateListeningState).launchIn(this)

speechRecognizer.events().onEach { event ->
speechRecognizerService.events().onEach { event ->
when (event) {
is SpeechEvent.Result -> executeVoiceCommands(event.matches)
is SpeechEvent.Error -> showToast(event.errorMessage)
is SpeechEvent.RmsChanged -> updateAudioInputLevel(event.rmsdB)
}
}.launchIn(this)

coroutineContext.job.invokeOnCompletion { speechRecognizer.destroy() }
coroutineContext.job.invokeOnCompletion { speechRecognizerService.destroy() }
}

private suspend fun executeVoiceCommands(words: List<String>) {
Expand All @@ -70,7 +70,7 @@ class SpeechRecognizerStateHolder(

private fun changePermissionsState(newPermissionState: PermissionState) {
if (newPermissionState.isEqualTo(PermissionState.Granted)) {
speechRecognizer.toggleListening()
speechRecognizerService.toggleListening()
updatePermissionStateTo(PermissionState.ConfirmationNeeded)
} else {
updatePermissionStateTo(newPermissionState)
Expand Down
Loading

0 comments on commit b801488

Please sign in to comment.