From 7b5b266c25c219200cdfa3080bfca849f6e598b9 Mon Sep 17 00:00:00 2001 From: Mohammad Nawas Date: Sat, 13 Jan 2024 19:02:32 +0200 Subject: [PATCH 1/5] News ticker alternative solution - using a lazy column --- .idea/.name | 1 + .idea/deploymentTargetDropDown.xml | 17 ++ .idea/misc.xml | 3 +- app/src/main/AndroidManifest.xml | 24 ++- .../main/java/com/loc/newsapp/MainActivity.kt | 47 +++- .../java/com/loc/newsapp/MainViewModel.kt | 37 ++++ .../java/com/loc/newsapp/NewsApplication.kt | 8 + .../com/loc/newsapp/data/local/NewsDao.kt | 25 +++ .../loc/newsapp/data/local/NewsDatabase.kt | 14 ++ .../newsapp/data/local/NewsTypeConvertor.kt | 22 ++ .../data/manger/LocalUserMangerImpl.kt | 35 +++ .../com/loc/newsapp/data/remote/NewsApi.kt | 25 +++ .../newsapp/data/remote/NewsPagingSource.kt | 39 ++++ .../data/remote/SearchNewsPagingSource.kt | 41 ++++ .../newsapp/data/remote/dto/NewsResponse.kt | 9 + .../data/repository/NewsRepositoryImpl.kt | 61 ++++++ .../main/java/com/loc/newsapp/di/AppModule.kt | 107 +++++++++ .../newsapp/domain/manger/LocalUserManger.kt | 10 + .../com/loc/newsapp/domain/model/Article.kt | 19 ++ .../com/loc/newsapp/domain/model/Source.kt | 10 + .../domain/repository/NewsRepository.kt | 20 ++ .../usecases/app_entry/AppEntryUseCases.kt | 6 + .../domain/usecases/app_entry/ReadAppEntry.kt | 14 ++ .../domain/usecases/app_entry/SaveAppEntry.kt | 13 ++ .../domain/usecases/news/DeleteArticle.kt | 15 ++ .../newsapp/domain/usecases/news/GetNews.kt | 16 ++ .../domain/usecases/news/NewsUseCases.kt | 10 + .../domain/usecases/news/SearchNews.kt | 16 ++ .../domain/usecases/news/SelectArticle.kt | 15 ++ .../domain/usecases/news/SelectArticles.kt | 16 ++ .../domain/usecases/news/UpsertArticle.kt | 15 ++ .../com/loc/newsapp/presentation/Dimens.kt | 26 +++ .../presentation/bookmark/BookmarkScreen.kt | 42 ++++ .../presentation/bookmark/BookmarkState.kt | 7 + .../bookmark/BookmarkViewModel.kt | 32 +++ .../presentation/common/ArticleCard.kt | 134 ++++++++++++ .../presentation/common/ArticlesList.kt | 97 +++++++++ .../presentation/common/EmptyScreen.kt | 117 ++++++++++ .../newsapp/presentation/common/NewsButton.kt | 55 +++++ .../newsapp/presentation/common/SearchBar.kt | 129 +++++++++++ .../presentation/common/ShimmerEffect.kt | 111 ++++++++++ .../presentation/details/DetailsEvent.kt | 11 + .../presentation/details/DetailsScreen.kt | 140 ++++++++++++ .../presentation/details/DetailsViewModel.kt | 51 +++++ .../details/components/DetailsTopBar.kt | 95 ++++++++ .../newsapp/presentation/home/HomeEvent.kt | 6 + .../newsapp/presentation/home/HomeScreen.kt | 142 ++++++++++++ .../newsapp/presentation/home/HomeState.kt | 6 + .../presentation/home/HomeViewModel.kt | 38 ++++ .../news_navigator/NewsNavigator.kt | 204 ++++++++++++++++++ .../components/NewsBottomNavigation.kt | 86 ++++++++ .../newsapp/presentation/nvgraph/NavGraph.kt | 45 ++++ .../loc/newsapp/presentation/nvgraph/Route.kt | 15 ++ .../onboarding/OnBoardingEvent.kt | 7 + .../onboarding/OnBoardingScreen.kt | 106 +++++++++ .../onboarding/OnBoardingViewModel.kt | 31 +++ .../newsapp/presentation/onboarding/Page.kt | 31 +++ .../onboarding/components/OnBoardingPage.kt | 69 ++++++ .../onboarding/components/PageIndicator.kt | 33 +++ .../presentation/search/SearchEvent.kt | 8 + .../presentation/search/SearchScreen.kt | 48 +++++ .../presentation/search/SearchState.kt | 11 + .../presentation/search/SearchViewModel.kt | 41 ++++ .../java/com/loc/newsapp/ui/theme/Theme.kt | 2 +- .../java/com/loc/newsapp/util/Constants.kt | 15 ++ app/src/main/res/values-night/splash.xml | 8 + app/src/main/res/values/splash.xml | 8 + app/src/main/res/values/themes.xml | 4 +- 68 files changed, 2707 insertions(+), 14 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 app/src/main/java/com/loc/newsapp/MainViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/NewsApplication.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt create mode 100644 app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt create mode 100644 app/src/main/java/com/loc/newsapp/di/AppModule.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/model/Article.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/model/Source.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/Dimens.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt create mode 100644 app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt create mode 100644 app/src/main/java/com/loc/newsapp/util/Constants.kt create mode 100644 app/src/main/res/values-night/splash.xml create mode 100644 app/src/main/res/values/splash.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..8a34a9b --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +NewsApp \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..e6fee04 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 9f71c83..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,6 @@ - - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 27cffd9..c6deea4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools"> + + + android:theme="@style/App.Starting.Theme" + tools:targetApi="31"> + android:theme="@style/App.Starting.Theme"> - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/MainActivity.kt b/app/src/main/java/com/loc/newsapp/MainActivity.kt index c001ad9..520cd07 100644 --- a/app/src/main/java/com/loc/newsapp/MainActivity.kt +++ b/app/src/main/java/com/loc/newsapp/MainActivity.kt @@ -3,21 +3,58 @@ package com.loc.newsapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize +import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.graphics.Color +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.model.Source +import com.loc.newsapp.presentation.nvgraph.NavGraph import com.loc.newsapp.ui.theme.NewsAppTheme +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject +@AndroidEntryPoint class MainActivity : ComponentActivity() { + val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + + + installSplashScreen().apply { + setKeepOnScreenCondition { + viewModel.splashCondition + } + } setContent { NewsAppTheme { + val isSystemInDarkMode = isSystemInDarkTheme() + val systemController = rememberSystemUiController() + + SideEffect { + systemController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = !isSystemInDarkMode + ) + } + + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { + val startDestination = viewModel.startDestination + NavGraph(startDestination = startDestination) + } } } } diff --git a/app/src/main/java/com/loc/newsapp/MainViewModel.kt b/app/src/main/java/com/loc/newsapp/MainViewModel.kt new file mode 100644 index 0000000..ef7df62 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/MainViewModel.kt @@ -0,0 +1,37 @@ +package com.loc.newsapp + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases +import com.loc.newsapp.presentation.nvgraph.Route +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject +@HiltViewModel +class MainViewModel @Inject constructor( + private val appEntryUseCases: AppEntryUseCases +) : ViewModel() { + + var splashCondition by mutableStateOf(true) + private set + + var startDestination by mutableStateOf(Route.AppStartNavigation.route) + private set + + init { + appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> + if(shouldStartFromHomeScreen){ + startDestination = Route.NewsNavigation.route + }else{ + startDestination = Route.AppStartNavigation.route + } + delay(300) + splashCondition= false + }.launchIn(viewModelScope) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/NewsApplication.kt b/app/src/main/java/com/loc/newsapp/NewsApplication.kt new file mode 100644 index 0000000..3aa9a6a --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/NewsApplication.kt @@ -0,0 +1,8 @@ +package com.loc.newsapp + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class NewsApplication: Application() { +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt b/app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt new file mode 100644 index 0000000..0d4e311 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt @@ -0,0 +1,25 @@ +package com.loc.newsapp.data.local + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.loc.newsapp.domain.model.Article +import kotlinx.coroutines.flow.Flow + +@Dao +interface NewsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(article: Article) + + @Delete + suspend fun delete(article: Article) + + @Query("SELECT * FROM Article") + fun getArticles(): Flow> + + @Query("SELECT * FROM Article WHERE url=:url") + suspend fun getArticle(url: String): Article? +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt b/app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt new file mode 100644 index 0000000..e4af0a2 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt @@ -0,0 +1,14 @@ +package com.loc.newsapp.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.loc.newsapp.domain.model.Article + +@Database(entities = [Article::class], version = 2) +@TypeConverters(NewsTypeConvertor::class) +abstract class NewsDatabase: RoomDatabase() { + + abstract val newsDao: NewsDao + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt b/app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt new file mode 100644 index 0000000..6e5a4e2 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt @@ -0,0 +1,22 @@ +package com.loc.newsapp.data.local + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import com.loc.newsapp.domain.model.Source + +@ProvidedTypeConverter +class NewsTypeConvertor { + + @TypeConverter + fun sourceToString(source: Source): String { + return "${source.id},${source.name}" + } + + @TypeConverter + fun stringToSource(source: String): Source { + return source.split(",").let { sourceArray -> + Source(sourceArray[0], sourceArray[1]) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt b/app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt new file mode 100644 index 0000000..52efad7 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt @@ -0,0 +1,35 @@ +package com.loc.newsapp.data.manger + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import com.loc.newsapp.domain.manger.LocalUserManger +import com.loc.newsapp.util.Constants +import com.loc.newsapp.util.Constants.USER_SETTINGS +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class LocalUserMangerImpl( + private val context: Context +) : LocalUserManger { + override suspend fun saveAppEntry() { + context.dataStore.edit { settings -> + settings[PreferencesKeys.APP_ENTRY] = true + } + } + + override fun readAppEntry(): Flow { + return context.dataStore.data.map { preferences -> + preferences[PreferencesKeys.APP_ENTRY] ?: false + } + } +} + +private val Context.dataStore: DataStore by preferencesDataStore(name = USER_SETTINGS) + +private object PreferencesKeys { + val APP_ENTRY = booleanPreferencesKey(name = Constants.APP_ENTRY) +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt b/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt new file mode 100644 index 0000000..9c59345 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt @@ -0,0 +1,25 @@ +package com.loc.newsapp.data.remote + +import com.loc.newsapp.data.remote.dto.NewsResponse +import com.loc.newsapp.util.Constants.API_KEY +import retrofit2.http.GET +import retrofit2.http.Query + +interface NewsApi { + + @GET("everything") + suspend fun getNews( + @Query("page") page: Int, + @Query("sources") sources: String, + @Query("apiKey") apiKey: String = API_KEY + ): NewsResponse + + @GET("everything") + suspend fun searchNews( + @Query("q") searchQuery: String, + @Query("page") page: Int, + @Query("sources") sources: String, + @Query("apiKey") apiKey: String = API_KEY + ): NewsResponse + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt b/app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt new file mode 100644 index 0000000..23e0dff --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt @@ -0,0 +1,39 @@ +package com.loc.newsapp.data.remote + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.loc.newsapp.domain.model.Article + +class NewsPagingSource( + private val newsApi: NewsApi, + private val sources: String +) : PagingSource() { + + private var totalNewsCount = 0 + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 1 + return try { + val newsResponse = newsApi.getNews(sources = sources, page = page) + totalNewsCount += newsResponse.articles.size + val articles = newsResponse.articles.distinctBy { it.title } // Remove Duplicates + LoadResult.Page( + data = articles, + nextKey = if (totalNewsCount == newsResponse.totalResults) null else page + 1, + prevKey = null + ) + } catch (e: Exception) { + e.printStackTrace() + LoadResult.Error( + throwable = e + ) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt b/app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt new file mode 100644 index 0000000..e43d71b --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt @@ -0,0 +1,41 @@ +package com.loc.newsapp.data.remote + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.loc.newsapp.domain.model.Article + +class SearchNewsPagingSource( + private val newsApi: NewsApi, + private val searchQuery: String, + private val sources: String +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + private var totalNewsCount = 0 + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 1 + return try { + val newsResponse = + newsApi.searchNews(searchQuery = searchQuery, sources = sources, page = page) + totalNewsCount += newsResponse.articles.size + val articles = newsResponse.articles.distinctBy { it.title } // Remove Duplicates + LoadResult.Page( + data = articles, + nextKey = if (totalNewsCount == newsResponse.totalResults) null else page + 1, + prevKey = null + ) + } catch (e: Exception) { + e.printStackTrace() + LoadResult.Error( + throwable = e + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt b/app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt new file mode 100644 index 0000000..1f19985 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt @@ -0,0 +1,9 @@ +package com.loc.newsapp.data.remote.dto + +import com.loc.newsapp.domain.model.Article + +data class NewsResponse( + val articles: List
, + val status: String, + val totalResults: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt b/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt new file mode 100644 index 0000000..f2b0e47 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt @@ -0,0 +1,61 @@ +package com.loc.newsapp.data.repository + +import android.util.Log +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.data.remote.NewsApi +import com.loc.newsapp.data.remote.NewsPagingSource +import com.loc.newsapp.data.remote.SearchNewsPagingSource +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +class NewsRepositoryImpl( + private val newsApi: NewsApi, + private val newsDao: NewsDao +) : NewsRepository { + + override fun getNews(sources: List): Flow> { + return Pager( + config = PagingConfig(pageSize = 10), + pagingSourceFactory = { + NewsPagingSource( + newsApi = newsApi, + sources = sources.joinToString(separator = ",") + ) + } + ).flow + } + + override fun searchNews(searchQuery: String, sources: List): Flow> { + return Pager( + config = PagingConfig(pageSize = 10), + pagingSourceFactory = { + SearchNewsPagingSource( + searchQuery = searchQuery, + newsApi = newsApi, + sources = sources.joinToString(separator = ",") + ) + } + ).flow + } + + override suspend fun upsertArticle(article: Article) { + newsDao.upsert(article) + } + + override suspend fun deleteArticle(article: Article) { + newsDao.delete(article) + } + + override fun selectArticles(): Flow> { + return newsDao.getArticles() + } + + override suspend fun selectArticle(url: String): Article? { + return newsDao.getArticle(url) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/di/AppModule.kt b/app/src/main/java/com/loc/newsapp/di/AppModule.kt new file mode 100644 index 0000000..d2e9386 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/di/AppModule.kt @@ -0,0 +1,107 @@ +package com.loc.newsapp.di + +import android.app.Application +import androidx.room.Room +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.data.local.NewsDatabase +import com.loc.newsapp.data.local.NewsTypeConvertor +import com.loc.newsapp.data.manger.LocalUserMangerImpl +import com.loc.newsapp.data.remote.NewsApi +import com.loc.newsapp.data.repository.NewsRepositoryImpl +import com.loc.newsapp.domain.manger.LocalUserManger +import com.loc.newsapp.domain.repository.NewsRepository +import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases +import com.loc.newsapp.domain.usecases.app_entry.ReadAppEntry +import com.loc.newsapp.domain.usecases.app_entry.SaveAppEntry +import com.loc.newsapp.domain.usecases.news.DeleteArticle +import com.loc.newsapp.domain.usecases.news.GetNews +import com.loc.newsapp.domain.usecases.news.NewsUseCases +import com.loc.newsapp.domain.usecases.news.SearchNews +import com.loc.newsapp.domain.usecases.news.SelectArticle +import com.loc.newsapp.domain.usecases.news.SelectArticles +import com.loc.newsapp.domain.usecases.news.UpsertArticle +import com.loc.newsapp.util.Constants.BASE_URL +import com.loc.newsapp.util.Constants.NEWS_DATABASE_NAME +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideLocalUserManger( + application: Application + ): LocalUserManger = LocalUserMangerImpl(application) + + + @Provides + @Singleton + fun provideAppEntryUseCases( + localUserManger: LocalUserManger + ) = AppEntryUseCases( + readAppEntry = ReadAppEntry(localUserManger), + saveAppEntry = SaveAppEntry(localUserManger) + ) + + @Provides + @Singleton + fun provideNewsApi(): NewsApi { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(NewsApi::class.java) + } + + @Provides + @Singleton + fun provideNewsRepository( + newsApi: NewsApi, + newsDao: NewsDao + ): NewsRepository = NewsRepositoryImpl(newsApi,newsDao) + + @Provides + @Singleton + fun provideNewsUseCases( + newsRepository: NewsRepository, + newsDao: NewsDao + ): NewsUseCases { + return NewsUseCases( + getNews = GetNews(newsRepository), + searchNews = SearchNews(newsRepository), + upsertArticle = UpsertArticle(newsRepository), + deleteArticle = DeleteArticle(newsRepository), + selectArticles = SelectArticles(newsRepository), + selectArticle = SelectArticle(newsRepository) + ) + } + + @Provides + @Singleton + fun provideNewsDatabase( + application: Application + ): NewsDatabase { + return Room.databaseBuilder( + context = application, + klass = NewsDatabase::class.java, + name = NEWS_DATABASE_NAME + ).addTypeConverter(NewsTypeConvertor()) + .fallbackToDestructiveMigration() + .build() + } + + @Provides + @Singleton + fun provideNewsDao( + newsDatabase: NewsDatabase + ): NewsDao = newsDatabase.newsDao + + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt b/app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt new file mode 100644 index 0000000..9577349 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt @@ -0,0 +1,10 @@ +package com.loc.newsapp.domain.manger + +import kotlinx.coroutines.flow.Flow + +interface LocalUserManger { + + suspend fun saveAppEntry() + + fun readAppEntry(): Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/model/Article.kt b/app/src/main/java/com/loc/newsapp/domain/model/Article.kt new file mode 100644 index 0000000..1c068cd --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/model/Article.kt @@ -0,0 +1,19 @@ +package com.loc.newsapp.domain.model + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity +data class Article( + val author: String?, + val content: String, + val description: String, + val publishedAt: String, + val source: Source, + val title: String, + @PrimaryKey val url: String, + val urlToImage: String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/model/Source.kt b/app/src/main/java/com/loc/newsapp/domain/model/Source.kt new file mode 100644 index 0000000..d3b86ee --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/model/Source.kt @@ -0,0 +1,10 @@ +package com.loc.newsapp.domain.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Source( + val id: String, + val name: String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt b/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt new file mode 100644 index 0000000..cfcee45 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt @@ -0,0 +1,20 @@ +package com.loc.newsapp.domain.repository + +import androidx.paging.PagingData +import com.loc.newsapp.domain.model.Article +import kotlinx.coroutines.flow.Flow + +interface NewsRepository { + + fun getNews(sources: List): Flow> + fun searchNews(searchQuery: String,sources: List): Flow> + + suspend fun upsertArticle(article: Article) + + suspend fun deleteArticle(article: Article) + + fun selectArticles(): Flow> + + suspend fun selectArticle(url: String): Article? + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt new file mode 100644 index 0000000..2a850bd --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt @@ -0,0 +1,6 @@ +package com.loc.newsapp.domain.usecases.app_entry + +data class AppEntryUseCases( + val readAppEntry: ReadAppEntry, + val saveAppEntry: SaveAppEntry +) diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt new file mode 100644 index 0000000..8dc3a36 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt @@ -0,0 +1,14 @@ +package com.loc.newsapp.domain.usecases.app_entry + +import com.loc.newsapp.domain.manger.LocalUserManger +import kotlinx.coroutines.flow.Flow + +class ReadAppEntry( + private val localUserManger: LocalUserManger +) { + + operator fun invoke(): Flow{ + return localUserManger.readAppEntry() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt new file mode 100644 index 0000000..f8a09a8 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt @@ -0,0 +1,13 @@ +package com.loc.newsapp.domain.usecases.app_entry + +import com.loc.newsapp.domain.manger.LocalUserManger + +class SaveAppEntry( + private val localUserManger: LocalUserManger +) { + + suspend operator fun invoke(){ + localUserManger.saveAppEntry() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt new file mode 100644 index 0000000..f992ab5 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt @@ -0,0 +1,15 @@ +package com.loc.newsapp.domain.usecases.news + +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository + +class DeleteArticle( + private val newsRepository: NewsRepository +) { + + suspend operator fun invoke(article: Article){ + newsRepository.deleteArticle(article) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt new file mode 100644 index 0000000..921d704 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt @@ -0,0 +1,16 @@ +package com.loc.newsapp.domain.usecases.news + +import androidx.paging.PagingData +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository +import kotlinx.coroutines.flow.Flow + +class GetNews( + private val newsRepository: NewsRepository +) { + + operator fun invoke(sources: List): Flow>{ + return newsRepository.getNews(sources = sources) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt new file mode 100644 index 0000000..4876ef6 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt @@ -0,0 +1,10 @@ +package com.loc.newsapp.domain.usecases.news + +data class NewsUseCases( + val getNews: GetNews, + val searchNews: SearchNews, + val upsertArticle: UpsertArticle, + val deleteArticle: DeleteArticle, + val selectArticles: SelectArticles, + val selectArticle: SelectArticle +) diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt new file mode 100644 index 0000000..7f3e08e --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt @@ -0,0 +1,16 @@ +package com.loc.newsapp.domain.usecases.news + +import androidx.paging.PagingData +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository +import kotlinx.coroutines.flow.Flow + +class SearchNews( + private val newsRepository: NewsRepository +) { + + operator fun invoke(searchQuery: String,sources: List): Flow> { + return newsRepository.searchNews(searchQuery = searchQuery,sources = sources) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt new file mode 100644 index 0000000..46d0b54 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt @@ -0,0 +1,15 @@ +package com.loc.newsapp.domain.usecases.news + +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository + +class SelectArticle( + private val newsRepository: NewsRepository +) { + + suspend operator fun invoke(url: String): Article?{ + return newsRepository.selectArticle(url) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt new file mode 100644 index 0000000..561496e --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt @@ -0,0 +1,16 @@ +package com.loc.newsapp.domain.usecases.news + +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository +import kotlinx.coroutines.flow.Flow + +class SelectArticles( + private val newsRepository: NewsRepository +) { + + operator fun invoke(): Flow>{ + return newsRepository.selectArticles() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt new file mode 100644 index 0000000..8a79bac --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt @@ -0,0 +1,15 @@ +package com.loc.newsapp.domain.usecases.news + +import com.loc.newsapp.data.local.NewsDao +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.repository.NewsRepository + +class UpsertArticle( + private val newsRepository: NewsRepository +) { + + suspend operator fun invoke(article: Article){ + newsRepository.upsertArticle(article) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/Dimens.kt b/app/src/main/java/com/loc/newsapp/presentation/Dimens.kt new file mode 100644 index 0000000..5543421 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/Dimens.kt @@ -0,0 +1,26 @@ +package com.loc.newsapp.presentation + +import androidx.compose.ui.unit.dp + +object Dimens { + + val MediumPadding1 = 24.dp + val MediumPadding2 = 30.dp + + val IndicatorSize = 14.dp + + val PageIndicatorWidth = 52.dp + + val ArticleCardSize = 96.dp + + val ExtraSmallPadding = 3.dp + + val ExtraSmallPadding2 = 6.dp + + val SmallIconSize = 11.dp + + val IconSize = 20.dp + + val ArticleImageHeight = 248.dp + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt new file mode 100644 index 0000000..3146131 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt @@ -0,0 +1,42 @@ +package com.loc.newsapp.presentation.bookmark + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import com.loc.newsapp.R +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.presentation.common.ArticlesList +import com.loc.newsapp.presentation.nvgraph.Route + +@Composable +fun BookmarkScreen( + state: BookmarkState, + navigateToDetails: (Article) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .padding(top = MediumPadding1, start = MediumPadding1, end = MediumPadding1) + ) { + Text( + text = "Bookmark", + style = MaterialTheme.typography.displayMedium.copy(fontWeight = FontWeight.Bold), + color = colorResource(id = R.color.text_title) + ) + + Spacer(modifier = Modifier.height(MediumPadding1)) + + ArticlesList(articles = state.articles, onClick = { navigateToDetails(it) }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt new file mode 100644 index 0000000..8571f11 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt @@ -0,0 +1,7 @@ +package com.loc.newsapp.presentation.bookmark + +import com.loc.newsapp.domain.model.Article + +data class BookmarkState( + val articles: List
= emptyList() +) diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt new file mode 100644 index 0000000..6687889 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt @@ -0,0 +1,32 @@ +package com.loc.newsapp.presentation.bookmark + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.loc.newsapp.domain.usecases.news.NewsUseCases +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@HiltViewModel +class BookmarkViewModel @Inject constructor( + private val newsUseCases: NewsUseCases +) : ViewModel() { + + + private val _state = mutableStateOf(BookmarkState()) + val state: State = _state + + init { + getArticles() + } + + private fun getArticles() { + newsUseCases.selectArticles().onEach { + _state.value = _state.value.copy(articles = it.asReversed()) + }.launchIn(viewModelScope) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt b/app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt new file mode 100644 index 0000000..306aa28 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt @@ -0,0 +1,134 @@ +package com.loc.newsapp.presentation.common + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.loc.newsapp.R +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.model.Source +import com.loc.newsapp.presentation.Dimens.ArticleCardSize +import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding +import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.presentation.Dimens.SmallIconSize +import com.loc.newsapp.ui.theme.NewsAppTheme + +@Composable +fun ArticleCard( + modifier: Modifier = Modifier, + article: Article, + onClick: () -> Unit +) { + val context = LocalContext.current + + Row(modifier = modifier.clickable { onClick() }) { + + AsyncImage( + modifier = Modifier + .size(ArticleCardSize) + .clip(MaterialTheme.shapes.medium), + model = ImageRequest.Builder(context).data(article.urlToImage).build(), + contentDescription = null, + contentScale = ContentScale.Crop + ) + + Column( + verticalArrangement = Arrangement.SpaceAround, + modifier = Modifier + .padding(horizontal = ExtraSmallPadding) + .height( + ArticleCardSize + ) + ) { + Text( + text = article.title, + style = MaterialTheme.typography.bodyMedium, + color = colorResource( + id = R.color.text_title + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = article.source.name, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold), + color = colorResource(id = R.color.body) + ) + + Spacer(modifier = Modifier.width(ExtraSmallPadding2)) + Icon( + painter = painterResource(id = R.drawable.ic_time), contentDescription = null, + modifier = Modifier.size(SmallIconSize), + tint = colorResource(id = R.color.body) + ) + Spacer(modifier = Modifier.width(ExtraSmallPadding2)) + Text( + text = article.publishedAt, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold), + color = colorResource(id = R.color.body) + ) + } + + } + + } + +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun ArticleCardPreview() { + NewsAppTheme { + ArticleCard( + article = Article( + author = "", + content = "", + description = "", + publishedAt = "2 hours", + source = Source(id = "", name = "BBC"), + title = "Her traint broke down. Her phone died. And then she met her saver in a", + url = "", + urlToImage = "" + ) + ) { + + } + } +} + + + + + + + + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt b/app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt new file mode 100644 index 0000000..893f520 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt @@ -0,0 +1,97 @@ +package com.loc.newsapp.presentation.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.presentation.Dimens.MediumPadding1 + +@Composable +fun ArticlesList( + modifier: Modifier = Modifier, + articles: List
, + onClick: (Article) -> Unit +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(MediumPadding1), + contentPadding = PaddingValues(all = ExtraSmallPadding2) + ) { + items(count = articles.size) { + val article = articles[it] + ArticleCard(article = article, onClick = { onClick(article) }) + } + } +} + +@Composable +fun ArticlesList( + modifier: Modifier = Modifier, + articles: LazyPagingItems
, + onClick: (Article) -> Unit +) { + val handlePagingResult = handlePagingResult(articles = articles) + if (handlePagingResult) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(MediumPadding1), + contentPadding = PaddingValues(all = ExtraSmallPadding2) + ) { + items(count = articles.itemCount) { + articles[it]?.let { + ArticleCard(article = it, onClick = { onClick(it) }) + } + } + } + } +} + +@Composable +fun handlePagingResult( + articles: LazyPagingItems
, +): Boolean { + + val loadState = articles.loadState + val error = when { + loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error + loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error + loadState.append is LoadState.Error -> loadState.append as LoadState.Error + else -> null + } + + return when { + loadState.refresh is LoadState.Loading -> { + ShimmerEffect() + false + } + + error != null -> { + EmptyScreen() + false + } + + else -> { + true + } + } + +} + +@Composable +private fun ShimmerEffect() { + Column(verticalArrangement = Arrangement.spacedBy(MediumPadding1)) { + repeat(10) { + ArticleCardShimmerEffect( + modifier = Modifier.padding(horizontal = MediumPadding1) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt new file mode 100644 index 0000000..2b0c9a9 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt @@ -0,0 +1,117 @@ +package com.loc.newsapp.presentation.common + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color.Companion.DarkGray +import androidx.compose.ui.graphics.Color.Companion.LightGray +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState + +import com.loc.newsapp.R +import java.net.ConnectException +import java.net.SocketTimeoutException + +@Composable +fun EmptyScreen(error: LoadState.Error? = null) { + + var message by remember { + mutableStateOf(parseErrorMessage(error = error)) + } + + var icon by remember { + mutableStateOf(R.drawable.ic_network_error) + } + + if (error == null){ + message = "You have not saved news so far !" + icon = R.drawable.ic_search_document + } + + var startAnimation by remember { + mutableStateOf(false) + } + + val alphaAnimation by animateFloatAsState( + targetValue = if (startAnimation) 0.3f else 0f, + animationSpec = tween(durationMillis = 1000) + ) + + LaunchedEffect(key1 = true) { + startAnimation = true + } + + EmptyContent(alphaAnim = alphaAnimation, message = message, iconId = icon) + +} + +@Composable +fun EmptyContent(alphaAnim: Float, message: String, iconId: Int) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = iconId), + contentDescription = null, + tint = if (isSystemInDarkTheme()) LightGray else DarkGray, + modifier = Modifier + .size(120.dp) + .alpha(alphaAnim) + ) + Text( + modifier = Modifier + .padding(10.dp) + .alpha(alphaAnim), + text = message, + style = MaterialTheme.typography.bodyMedium, + color = if (isSystemInDarkTheme()) LightGray else DarkGray, + ) + } +} + + +fun parseErrorMessage(error: LoadState.Error?): String { + return when (error?.error) { + is SocketTimeoutException -> { + "Server Unavailable." + } + + is ConnectException -> { + "Internet Unavailable." + } + + else -> { + "Unknown Error." + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun EmptyScreenPreview() { + EmptyContent(alphaAnim = 0.3f, message = "Internet Unavailable.",R.drawable.ic_network_error) +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt b/app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt new file mode 100644 index 0000000..28fec76 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt @@ -0,0 +1,55 @@ +package com.loc.newsapp.presentation.common + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.loc.newsapp.ui.theme.WhiteGray + +@Composable +fun NewsButton( + text: String, + onClick: () -> Unit +) { + + Button( + onClick = onClick, colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = Color.White + ), + shape = RoundedCornerShape(size = 6.dp) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) + ) + } +} + + +@Composable +fun NewsTextButton( + text: String, + onClick: () -> Unit +) { + TextButton(onClick = onClick) { + Text( + text = text, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = WhiteGray + ) + } +} + + + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt b/app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt new file mode 100644 index 0000000..faab865 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt @@ -0,0 +1,129 @@ +package com.loc.newsapp.presentation.common + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.loc.newsapp.R +import com.loc.newsapp.presentation.Dimens.IconSize +import com.loc.newsapp.ui.theme.NewsAppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBar( + modifier: Modifier = Modifier, + text: String, + readOnly: Boolean, + onClick: (() -> Unit)? = null, + onValueChange: (String) -> Unit, + onSearch: () -> Unit +) { + + val interactionSource = remember { + MutableInteractionSource() + } + val isClicked = interactionSource.collectIsPressedAsState().value + LaunchedEffect(key1 = isClicked) { + if (isClicked) { + onClick?.invoke() + } + } + + Box(modifier = modifier) { + + TextField( + modifier = Modifier + .fillMaxWidth() + .searchBarBorder(), + value = text, + onValueChange = onValueChange, + readOnly = readOnly, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = null, + modifier = Modifier.size(IconSize), + tint = colorResource(id = R.color.body) + ) + }, + placeholder = { + Text( + text = "Search", + style = MaterialTheme.typography.bodySmall, + color = colorResource( + id = R.color.placeholder + ) + ) + }, + shape = MaterialTheme.shapes.medium, + colors = TextFieldDefaults.textFieldColors( + containerColor = colorResource(id = R.color.input_background), + textColor = if (isSystemInDarkTheme()) Color.White else Color.Black, + cursorColor = if (isSystemInDarkTheme()) Color.White else Color.Black, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + onSearch() + } + ), + textStyle = MaterialTheme.typography.bodySmall, + interactionSource = interactionSource + ) + + + } + + +} + +fun Modifier.searchBarBorder() = composed { + if (!isSystemInDarkTheme()) { + border( + width = 1.dp, + color = Color.Black, + shape = MaterialTheme.shapes.medium + ) + } else { + this + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun SearchBarPreview() { + NewsAppTheme { + SearchBar(text = "", readOnly = false, onValueChange = {}) { + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt b/app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt new file mode 100644 index 0000000..d6ef47c --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt @@ -0,0 +1,111 @@ +package com.loc.newsapp.presentation.common + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.animation.core.RepeatMode +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.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.loc.newsapp.R +import com.loc.newsapp.presentation.Dimens +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.ui.theme.NewsAppTheme + +fun Modifier.shimmerEffect() = composed { + val transition = rememberInfiniteTransition() + val alpha = transition.animateFloat( + initialValue = 0.2f, + targetValue = 0.9f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1000), + repeatMode = RepeatMode.Reverse + ) + ).value + background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha)) +} + + +@Composable +fun ArticleCardShimmerEffect( + modifier: Modifier = Modifier +) { + + Row(modifier = modifier) { + + Box( + modifier = Modifier + .size(Dimens.ArticleCardSize) + .clip(MaterialTheme.shapes.medium) + .shimmerEffect(), + + ) + + Column( + verticalArrangement = Arrangement.SpaceAround, + modifier = Modifier + .padding(horizontal = Dimens.ExtraSmallPadding) + .height( + Dimens.ArticleCardSize + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(30.dp) + .padding(horizontal = MediumPadding1) + .shimmerEffect(), + + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .fillMaxWidth(0.5f) + .height(15.dp) + .padding(horizontal = MediumPadding1) + .shimmerEffect(), + + ) + } + + } + + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun ArticleCardShimmerEffectPreview() { + NewsAppTheme { + ArticleCardShimmerEffect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt new file mode 100644 index 0000000..056919f --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt @@ -0,0 +1,11 @@ +package com.loc.newsapp.presentation.details + +import com.loc.newsapp.domain.model.Article + +sealed class DetailsEvent { + + data class UpsertDeleteArticle(val article: Article) : DetailsEvent() + + object RemoveSideEffect : DetailsEvent() + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt new file mode 100644 index 0000000..71fcd94 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt @@ -0,0 +1,140 @@ +package com.loc.newsapp.presentation.details + +import android.content.Intent +import android.net.Uri +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.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.loc.newsapp.R +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.model.Source +import com.loc.newsapp.presentation.Dimens.ArticleImageHeight +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.presentation.details.components.DetailsTopBar +import com.loc.newsapp.ui.theme.NewsAppTheme + +@Composable +fun DetailsScreen( + article: Article, + event: (DetailsEvent) -> Unit, + navigateUp: () -> Unit +) { + + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + ) { + DetailsTopBar( + onBrowsingClick = { + Intent(Intent.ACTION_VIEW).also { + it.data = Uri.parse(article.url) + if (it.resolveActivity(context.packageManager) != null) { + context.startActivity(it) + } + } + }, + onShareClick = { + Intent(Intent.ACTION_SEND).also { + it.putExtra(Intent.EXTRA_TEXT, article.url) + it.type = "text/plain" + if (it.resolveActivity(context.packageManager) != null) { + context.startActivity(it) + } + } + }, + onBookmarkClick = { event(DetailsEvent.UpsertDeleteArticle(article)) }, + onBackClick = navigateUp + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues( + start = MediumPadding1, + end = MediumPadding1, + top = MediumPadding1 + ) + ) { + item { + + AsyncImage( + model = ImageRequest.Builder(context = context).data(article.urlToImage) + .build(), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(ArticleImageHeight) + .clip(MaterialTheme.shapes.medium), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(MediumPadding1)) + + Text( + text = article.title, + style = MaterialTheme.typography.displaySmall, + color = colorResource( + id = R.color.text_title + ) + ) + + Text( + text = article.content, + style = MaterialTheme.typography.bodyMedium, + color = colorResource( + id = R.color.body + ) + ) + } + } + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsScreenPreview() { + NewsAppTheme(dynamicColor = false) { + DetailsScreen( + article = Article( + author = "", + title = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", + description = "Coinbase says Apple blocked its last app release on NFTs in Wallet ... - CryptoSaurus", + content = "We use cookies and data to Deliver and maintain Google services Track outages and protect against spam, fraud, and abuse Measure audience engagement and site statistics to unde… [+1131 chars]", + publishedAt = "2023-06-16T22:24:33Z", + source = Source( + id = "", name = "bbc" + ), + url = "https://consent.google.com/ml?continue=https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vY3J5cHRvc2F1cnVzLnRlY2gvY29pbmJhc2Utc2F5cy1hcHBsZS1ibG9ja2VkLWl0cy1sYXN0LWFwcC1yZWxlYXNlLW9uLW5mdHMtaW4td2FsbGV0LXJldXRlcnMtY29tL9IBAA?oc%3D5&gl=FR&hl=en-US&cm=2&pc=n&src=1", + urlToImage = "https://media.wired.com/photos/6495d5e893ba5cd8bbdc95af/191:100/w_1280,c_limit/The-EU-Rules-Phone-Batteries-Must-Be-Replaceable-Gear-2BE6PRN.jpg" + ), + event = {} + ) { + + } + } +} + + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt new file mode 100644 index 0000000..038e3dd --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt @@ -0,0 +1,51 @@ +package com.loc.newsapp.presentation.details + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.usecases.news.NewsUseCases +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DetailsViewModel @Inject constructor( + private val newsUseCases: NewsUseCases +) : ViewModel() { + + var sideEffect by mutableStateOf(null) + private set + + fun onEvent(event: DetailsEvent) { + when (event) { + is DetailsEvent.UpsertDeleteArticle -> { + viewModelScope.launch { + val article = newsUseCases.selectArticle(event.article.url) + if (article == null) { + upsertArticle(event.article) + } else { + deleteArticle(event.article) + } + } + } + + is DetailsEvent.RemoveSideEffect -> { + sideEffect = null + } + } + } + + private suspend fun deleteArticle(article: Article) { + newsUseCases.deleteArticle(article = article) + sideEffect = "Article Deleted" + } + + private suspend fun upsertArticle(article: Article) { + newsUseCases.upsertArticle(article = article) + sideEffect = "Article Saved" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt b/app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt new file mode 100644 index 0000000..e29779f --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt @@ -0,0 +1,95 @@ +package com.loc.newsapp.presentation.details.components + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.loc.newsapp.R +import com.loc.newsapp.ui.theme.NewsAppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailsTopBar( + onBrowsingClick: () -> Unit, + onShareClick: () -> Unit, + onBookmarkClick: () -> Unit, + onBackClick: () -> Unit +) { + + TopAppBar( + title = { }, + modifier = Modifier.fillMaxWidth(), + colors = TopAppBarDefaults.mediumTopAppBarColors( + containerColor = Color.Transparent, + actionIconContentColor = colorResource(id = R.color.body), + navigationIconContentColor = colorResource(id = R.color.body), + ), + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_arrow), + contentDescription = null + ) + } + }, + actions = { + IconButton(onClick = onBookmarkClick) { + Icon( + painter = painterResource(id = R.drawable.ic_bookmark), + contentDescription = null + ) + } + IconButton(onClick = onShareClick) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = null + ) + } + IconButton(onClick = onBrowsingClick) { + Icon( + painter = painterResource(id = R.drawable.ic_network), + contentDescription = null + ) + } + } + ) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun DetailsTopBarPreivew() { + NewsAppTheme { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { + DetailsTopBar( + onBrowsingClick = { /*TODO*/ }, + onShareClick = { /*TODO*/ }, + onBookmarkClick = { /*TODO*/ }) { + } + } + } +} + + + + + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt b/app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt new file mode 100644 index 0000000..3037ffc --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt @@ -0,0 +1,6 @@ +package com.loc.newsapp.presentation.home + +sealed class HomeEvent { + data class UpdateScrollValue(val newValue: Int): HomeEvent() + data class UpdateMaxScrollingValue(val newValue: Int): HomeEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt new file mode 100644 index 0000000..72d3646 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt @@ -0,0 +1,142 @@ +package com.loc.newsapp.presentation.home + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.paging.compose.LazyPagingItems +import com.loc.newsapp.domain.model.Article +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.loc.newsapp.R +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.presentation.common.ArticlesList +import com.loc.newsapp.presentation.common.SearchBar +import com.loc.newsapp.presentation.nvgraph.Route +import com.loc.newsapp.ui.theme.NewsAppTheme +import kotlinx.coroutines.delay + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HomeScreen( + state: HomeState, + articles: LazyPagingItems
, + navigateToSearch: () -> Unit, + navigateToDetails: (Article) -> Unit, + event: (HomeEvent) -> Unit, +) { + val titles by remember { + derivedStateOf { + if (articles.itemCount > 10) { + articles.itemSnapshotList.items + .slice(IntRange(start = 0, endInclusive = 9)) + .joinToString(separator = " \uD83d\uDFE5 ") { it.title } + } else { + "" + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = MediumPadding1) + .statusBarsPadding() + ) { + Image( + painter = painterResource(id = R.drawable.ic_logo), + contentDescription = null, + modifier = Modifier + .width(150.dp) + .height(30.dp) + .padding(horizontal = MediumPadding1) + ) + + Spacer(modifier = Modifier.height(MediumPadding1)) + + SearchBar( + modifier = Modifier.padding(horizontal = MediumPadding1), + text = "", + readOnly = true, + onValueChange = {}, + onClick = { + navigateToSearch() + }, + onSearch = {} + ) + + Spacer(modifier = Modifier.height(MediumPadding1)) + + val scrollState = rememberScrollState() + + LaunchedEffect(key1 = state.maxScrollingValue) { + delay(500) + if (state.maxScrollingValue > 0) { + scrollState.animateScrollTo( + value = state.maxScrollingValue, + animationSpec = infiniteRepeatable( + tween( + durationMillis = (state.maxScrollingValue - state.scrollValue) * 50_000 / state.maxScrollingValue, + easing = LinearEasing, + delayMillis = 1000 + ) + ) + ) + } + } + // Update the maxScrollingValue + LaunchedEffect(key1 = scrollState.maxValue) { + event(HomeEvent.UpdateMaxScrollingValue(scrollState.maxValue)) + } + // Save the state of the scrolling position + LaunchedEffect(key1 = scrollState.value) { + event(HomeEvent.UpdateScrollValue(scrollState.value)) + } + + Text( + text = titles, + modifier = Modifier + .fillMaxWidth() + .padding(start = MediumPadding1) + .horizontalScroll(scrollState,false), + fontSize = 12.sp, + color = colorResource(id = R.color.placeholder) + ) + + Spacer(modifier = Modifier.height(MediumPadding1)) + + ArticlesList( + modifier = Modifier.padding(horizontal = MediumPadding1), + articles = articles, + onClick = { + navigateToDetails(it) + } + ) + } +} + diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt b/app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt new file mode 100644 index 0000000..20833dc --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt @@ -0,0 +1,6 @@ +package com.loc.newsapp.presentation.home + +data class HomeState( + val scrollValue: Int = 0, + val maxScrollingValue: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt b/app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt new file mode 100644 index 0000000..5ad026b --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt @@ -0,0 +1,38 @@ +package com.loc.newsapp.presentation.home + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.loc.newsapp.domain.usecases.news.NewsUseCases +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val newsUseCases: NewsUseCases +) : ViewModel() { + + private val _state = mutableStateOf(HomeState()) + val state: State = _state + + val news = newsUseCases.getNews( + sources = listOf("bbc-news", "abc-news", "al-jazeera-english") + ).cachedIn(viewModelScope) + fun onEvent(event: HomeEvent){ + when(event){ + is HomeEvent.UpdateScrollValue -> updateScrollValue(event.newValue) + is HomeEvent.UpdateMaxScrollingValue -> updateMaxScrollingValue(event.newValue) + } + } + private fun updateScrollValue(newValue: Int){ + _state.value = state.value.copy(scrollValue = newValue) + } + private fun updateMaxScrollingValue(newValue: Int){ + _state.value = state.value.copy(maxScrollingValue = newValue) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt b/app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt new file mode 100644 index 0000000..abaee2a --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt @@ -0,0 +1,204 @@ +package com.loc.newsapp.presentation.news_navigator + +import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.collectAsLazyPagingItems +import com.loc.newsapp.R +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.presentation.bookmark.BookmarkScreen +import com.loc.newsapp.presentation.bookmark.BookmarkViewModel +import com.loc.newsapp.presentation.details.DetailsEvent +import com.loc.newsapp.presentation.details.DetailsScreen +import com.loc.newsapp.presentation.details.DetailsViewModel +import com.loc.newsapp.presentation.home.HomeScreen +import com.loc.newsapp.presentation.home.HomeViewModel +import com.loc.newsapp.presentation.news_navigator.components.BottomNavigationItem +import com.loc.newsapp.presentation.news_navigator.components.NewsBottomNavigation +import com.loc.newsapp.presentation.nvgraph.Route +import com.loc.newsapp.presentation.search.SearchScreen +import com.loc.newsapp.presentation.search.SearchViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NewsNavigator() { + + val bottomNavigationItems = remember { + listOf( + BottomNavigationItem(icon = R.drawable.ic_home, text = "Home"), + BottomNavigationItem(icon = R.drawable.ic_search, text = "Search"), + BottomNavigationItem(icon = R.drawable.ic_bookmark, text = "Bookmark") + ) + } + + val navController = rememberNavController() + val backstackState = navController.currentBackStackEntryAsState().value + var selectedItem by rememberSaveable { + mutableStateOf(0) + } + + selectedItem = remember(key1 = backstackState) { + when (backstackState?.destination?.route) { + Route.HomeScreen.route -> 0 + Route.SearchScreen.route -> 1 + Route.BookmarkScreen.route -> 2 + else -> 0 + } + + } + + + val isBottomBarVisible = remember(key1 = backstackState) { + backstackState?.destination?.route == Route.HomeScreen.route || + backstackState?.destination?.route == Route.SearchScreen.route || + backstackState?.destination?.route == Route.BookmarkScreen.route + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + if (isBottomBarVisible) { + NewsBottomNavigation( + items = bottomNavigationItems, + selected = selectedItem, + onItemClick = { index -> + when (index) { + 0 -> navigateToTap( + navController = navController, + route = Route.HomeScreen.route + ) + + 1 -> navigateToTap( + navController = navController, + route = Route.SearchScreen.route + ) + + 2 -> navigateToTap( + navController = navController, + route = Route.BookmarkScreen.route + ) + } + } + ) + } + } + ) { + val bottomPadding = it.calculateBottomPadding() + NavHost( + navController = navController, + startDestination = Route.HomeScreen.route, + modifier = Modifier.padding(bottom = bottomPadding) + ) { + composable(route = Route.HomeScreen.route) { + val viewModel: HomeViewModel = hiltViewModel() + val articles = viewModel.news.collectAsLazyPagingItems() + val state by viewModel.state + HomeScreen( + articles = articles, + navigateToSearch = { + navigateToTap( + navController = navController, + route = Route.SearchScreen.route + ) + }, + navigateToDetails = { article -> + navigateToDetails( + navController = navController, + article = article + ) + }, + state = state, + event = { event -> + viewModel.onEvent(event) + } + ) + } + + composable(route = Route.SearchScreen.route) { + val viewModel: SearchViewModel = hiltViewModel() + val state = viewModel.state.value + SearchScreen( + state = state, + event = viewModel::onEvent, + navigateToDetails = { article -> + navigateToDetails( + navController = navController, + article = article + ) + } + ) + } + + composable(route = Route.DetailsScreen.route) { + val viewModel: DetailsViewModel = hiltViewModel() + if (viewModel.sideEffect != null) { + Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT) + .show() + viewModel.onEvent(DetailsEvent.RemoveSideEffect) + } + navController.previousBackStackEntry?.savedStateHandle?.get("article") + ?.let { article -> + DetailsScreen( + article = article, + event = viewModel::onEvent, + navigateUp = { navController.navigateUp() }) + } + } + + composable(route = Route.BookmarkScreen.route) { + val viewModel: BookmarkViewModel = hiltViewModel() + val state = viewModel.state.value + BookmarkScreen(state = state, navigateToDetails = { article -> + navigateToDetails(navController = navController, article = article) + + }) + } + } + } +} + +private fun navigateToTap(navController: NavController, route: String) { + navController.navigate(route) { + navController.graph.startDestinationRoute?.let { homeScreen -> + popUpTo(homeScreen) { + saveState = true + } + restoreState = true + launchSingleTop = true + } + } +} + +private fun navigateToDetails(navController: NavController, article: Article) { + navController.currentBackStackEntry?.savedStateHandle?.set("article", article) + navController.navigate( + route = Route.DetailsScreen.route + ) +} + + + + + + + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt b/app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt new file mode 100644 index 0000000..51ff854 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt @@ -0,0 +1,86 @@ +package com.loc.newsapp.presentation.news_navigator.components + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.loc.newsapp.R +import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.presentation.Dimens.IconSize +import com.loc.newsapp.ui.theme.NewsAppTheme + +@Composable +fun NewsBottomNavigation( + items: List, + selected: Int, + onItemClick: (Int) -> Unit +) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 10.dp + ) { + items.forEachIndexed { index, item -> + NavigationBarItem( + selected = index == selected, + onClick = { onItemClick(index) }, + icon = { + Column(horizontalAlignment = CenterHorizontally) { + Icon( + painter = painterResource(id = item.icon), + contentDescription = null, + modifier = Modifier.size(IconSize) + ) + Spacer(modifier = Modifier.height(ExtraSmallPadding2)) + Text(text = item.text, style = MaterialTheme.typography.labelSmall) + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = colorResource(id = R.color.body), + unselectedTextColor = colorResource(id = R.color.body), + indicatorColor = MaterialTheme.colorScheme.background + ) + ) + } + } +} + +data class BottomNavigationItem( + @DrawableRes val icon: Int, + val text: String +) + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +fun NewsBottomNavigationPreview() { + NewsAppTheme { + NewsBottomNavigation( + items = listOf( + BottomNavigationItem(icon = R.drawable.ic_home, text = "Home"), + BottomNavigationItem(icon = R.drawable.ic_search, text = "Search"), + BottomNavigationItem(icon = R.drawable.ic_bookmark, text = "Bookmark")), + selected = 0, + onItemClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt b/app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt new file mode 100644 index 0000000..68a4629 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt @@ -0,0 +1,45 @@ +package com.loc.newsapp.presentation.nvgraph + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navigation +import com.loc.newsapp.presentation.bookmark.BookmarkScreen +import com.loc.newsapp.presentation.bookmark.BookmarkViewModel +import com.loc.newsapp.presentation.news_navigator.NewsNavigator +import com.loc.newsapp.presentation.onboarding.OnBoardingScreen +import com.loc.newsapp.presentation.onboarding.OnBoardingViewModel + +@Composable +fun NavGraph( + startDestination: String +) { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = startDestination) { + navigation( + route = Route.AppStartNavigation.route, + startDestination = Route.OnBoardingScreen.route + ) { + composable( + route = Route.OnBoardingScreen.route + ) { + val viewModel: OnBoardingViewModel = hiltViewModel() + OnBoardingScreen( + event = viewModel::onEvent + ) + } + } + + navigation( + route = Route.NewsNavigation.route, + startDestination = Route.NewsNavigatorScreen.route + ) { + composable(route = Route.NewsNavigatorScreen.route) { + NewsNavigator() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt b/app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt new file mode 100644 index 0000000..9f2da37 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt @@ -0,0 +1,15 @@ +package com.loc.newsapp.presentation.nvgraph + +sealed class Route( + val route: String +){ + + object OnBoardingScreen : Route(route = "onBoardingScreen") + object HomeScreen : Route(route = "homeScreen") + object SearchScreen : Route(route = "searchScreen") + object BookmarkScreen : Route(route = "bookmarkScreen") + object DetailsScreen : Route(route = "detailsScreen") + object AppStartNavigation : Route(route = "appStartNavigation") + object NewsNavigation : Route(route = "newsNavigation") + object NewsNavigatorScreen : Route(route = "newsNavigator") +} diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt new file mode 100644 index 0000000..c845e90 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt @@ -0,0 +1,7 @@ +package com.loc.newsapp.presentation.onboarding + +sealed class OnBoardingEvent { + + object SaveAppEntry: OnBoardingEvent() + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt new file mode 100644 index 0000000..2d315f0 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt @@ -0,0 +1,106 @@ +package com.loc.newsapp.presentation.onboarding + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.loc.newsapp.presentation.Dimens.MediumPadding2 +import com.loc.newsapp.presentation.Dimens.PageIndicatorWidth +import com.loc.newsapp.presentation.common.NewsButton +import com.loc.newsapp.presentation.common.NewsTextButton +import com.loc.newsapp.presentation.onboarding.components.OnBoardingPage +import com.loc.newsapp.presentation.onboarding.components.PageIndicator +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OnBoardingScreen( + event: (OnBoardingEvent) -> Unit +) { + Column(modifier = Modifier.fillMaxSize()) { + val pagerState = rememberPagerState(initialPage = 0) { + pages.size + } + val buttonState = remember { + derivedStateOf { + when (pagerState.currentPage) { + 0 -> listOf("", "Next") + 1 -> listOf("Back", "Next") + 2 -> listOf("Back", "Get Started") + else -> listOf("", "") + } + } + } + + HorizontalPager(state = pagerState) { index -> + OnBoardingPage(page = pages[index]) + } + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = MediumPadding2) + .navigationBarsPadding(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + PageIndicator( + modifier = Modifier.width(PageIndicatorWidth), + pageSize = pages.size, + selectedPage = pagerState.currentPage + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + + val scope = rememberCoroutineScope() + + if (buttonState.value[0].isNotEmpty()) { + NewsTextButton( + text = buttonState.value[0], + onClick = { + scope.launch { + pagerState.animateScrollToPage(page = pagerState.currentPage - 1) + } + } + ) + } + + NewsButton( + text = buttonState.value[1], + onClick = { + scope.launch { + if (pagerState.currentPage == 2) { + event(OnBoardingEvent.SaveAppEntry) + } else { + pagerState.animateScrollToPage( + page = pagerState.currentPage + 1 + ) + } + } + } + ) + } + } + Spacer(modifier = Modifier.weight(0.5f)) + } +} + + + + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt new file mode 100644 index 0000000..98a38ea --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt @@ -0,0 +1,31 @@ +package com.loc.newsapp.presentation.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OnBoardingViewModel @Inject constructor( + private val appEntryUseCases: AppEntryUseCases +): ViewModel() { + + + fun onEvent(event: OnBoardingEvent){ + when(event){ + is OnBoardingEvent.SaveAppEntry -> { + saveAppEntry() + } + } + } + + private fun saveAppEntry() { + viewModelScope.launch { + appEntryUseCases.saveAppEntry() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt new file mode 100644 index 0000000..0702fdc --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt @@ -0,0 +1,31 @@ +package com.loc.newsapp.presentation.onboarding + +import androidx.annotation.DrawableRes +import com.loc.newsapp.R + +data class Page( + val title: String, + val description: String, + @DrawableRes val image: Int +) + + +val pages = listOf( + Page( + title = "Lorem Ipsum is simply dummy", + description = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + image = R.drawable.onboarding1 + ), + Page( + title = "Lorem Ipsum is simply dummy", + description = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + image = R.drawable.onboarding2 + ), + Page( + title = "Lorem Ipsum is simply dummy", + description = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + image = R.drawable.onboarding3 + ) +) + + diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt new file mode 100644 index 0000000..dabdf34 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt @@ -0,0 +1,69 @@ +package com.loc.newsapp.presentation.onboarding.components + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import com.loc.newsapp.R +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.presentation.Dimens.MediumPadding2 +import com.loc.newsapp.presentation.onboarding.Page +import com.loc.newsapp.presentation.onboarding.pages +import com.loc.newsapp.ui.theme.NewsAppTheme + +@Composable +fun OnBoardingPage( + modifier: Modifier = Modifier, + page: Page +) { + + Column(modifier = modifier) { + Image( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(fraction = 0.6f), + painter = painterResource(id = page.image), + contentDescription = null, + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(MediumPadding1)) + Text( + text = page.title, + modifier = Modifier.padding(horizontal = MediumPadding2), + style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), + color = colorResource(id = R.color.display_small) + ) + Text( + text = page.description, + modifier = Modifier.padding(horizontal = MediumPadding2), + style = MaterialTheme.typography.bodyMedium, + color = colorResource(id = R.color.text_medium) + ) + } + +} + + +@Preview(showBackground = true) +@Preview(uiMode = UI_MODE_NIGHT_YES,showBackground = true) +@Composable +fun OnBoardingPagePreview() { + NewsAppTheme { + OnBoardingPage( + page = pages[0] + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt b/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt new file mode 100644 index 0000000..cd44957 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt @@ -0,0 +1,33 @@ +package com.loc.newsapp.presentation.onboarding.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import com.loc.newsapp.presentation.Dimens.IndicatorSize +import com.loc.newsapp.ui.theme.BlueGray + +@Composable +fun PageIndicator( + modifier: Modifier = Modifier, + pageSize: Int, + selectedPage: Int, + selectedColor: Color = MaterialTheme.colorScheme.primary, + unselectedColor: Color = BlueGray +) { + Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) { + repeat(pageSize) { page -> + Box( + modifier = Modifier.size(IndicatorSize).clip(CircleShape) + .background(color = if (page == selectedPage) selectedColor else unselectedColor) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt b/app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt new file mode 100644 index 0000000..0c98548 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt @@ -0,0 +1,8 @@ +package com.loc.newsapp.presentation.search + +sealed class SearchEvent { + + data class UpdateSearchQuery(val searchQuery: String): SearchEvent() + + object SearchNews : SearchEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt b/app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt new file mode 100644 index 0000000..ec85a23 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt @@ -0,0 +1,48 @@ +package com.loc.newsapp.presentation.search + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.paging.compose.collectAsLazyPagingItems +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.presentation.common.ArticlesList +import com.loc.newsapp.presentation.common.SearchBar +import com.loc.newsapp.presentation.nvgraph.Route + +@Composable +fun SearchScreen( + state: SearchState, + event: (SearchEvent) -> Unit, + navigateToDetails: (Article) -> Unit +) { + + Column( + modifier = Modifier + .padding( + top = MediumPadding1, + start = MediumPadding1, + end = MediumPadding1 + ) + .statusBarsPadding() + .fillMaxSize() + ) { + SearchBar( + text = state.searchQuery, + readOnly = false, + onValueChange = { event(SearchEvent.UpdateSearchQuery(it)) }, + onSearch = { event(SearchEvent.SearchNews) }) + + Spacer(modifier = Modifier.height(MediumPadding1)) + state.articles?.let { + val articles = it.collectAsLazyPagingItems() + ArticlesList(articles = articles, onClick = { navigateToDetails(it) }) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt b/app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt new file mode 100644 index 0000000..366af35 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt @@ -0,0 +1,11 @@ +package com.loc.newsapp.presentation.search + +import androidx.paging.PagingData +import com.loc.newsapp.domain.model.Article +import kotlinx.coroutines.flow.Flow + +data class SearchState( + val searchQuery: String = "", + val articles: Flow>? = null +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt b/app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt new file mode 100644 index 0000000..62de4e6 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt @@ -0,0 +1,41 @@ +package com.loc.newsapp.presentation.search + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.loc.newsapp.domain.usecases.news.NewsUseCases +import com.loc.newsapp.domain.usecases.news.SearchNews +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val newsUseCases: NewsUseCases +) : ViewModel() { + + private val _state = mutableStateOf(SearchState()) + val state: State = _state + + fun onEvent(event: SearchEvent) { + when (event) { + is SearchEvent.UpdateSearchQuery -> { + _state.value = state.value.copy(searchQuery = event.searchQuery) + } + + is SearchEvent.SearchNews -> { + searchNews() + } + } + } + + private fun searchNews() { + val articles = newsUseCases.searchNews( + searchQuery = state.value.searchQuery, + sources = listOf("bbc-news", "abc-news", "al-jazeera-english") + ).cachedIn(viewModelScope) + _state.value = state.value.copy(articles = articles) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/ui/theme/Theme.kt b/app/src/main/java/com/loc/newsapp/ui/theme/Theme.kt index 3191bd0..c68b97f 100644 --- a/app/src/main/java/com/loc/newsapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/loc/newsapp/ui/theme/Theme.kt @@ -34,7 +34,7 @@ private val LightColorScheme = lightColorScheme( fun NewsAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { diff --git a/app/src/main/java/com/loc/newsapp/util/Constants.kt b/app/src/main/java/com/loc/newsapp/util/Constants.kt new file mode 100644 index 0000000..1086178 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/util/Constants.kt @@ -0,0 +1,15 @@ +package com.loc.newsapp.util + +object Constants { + + const val USER_SETTINGS = "userSettings" + + const val APP_ENTRY = "appEntry" + + const val API_KEY = "fee38bf418b04948b7b372c045fdf499" + + const val BASE_URL = "https://newsapi.org/v2/" + + const val NEWS_DATABASE_NAME = "news_db" + +} \ No newline at end of file diff --git a/app/src/main/res/values-night/splash.xml b/app/src/main/res/values-night/splash.xml new file mode 100644 index 0000000..2871686 --- /dev/null +++ b/app/src/main/res/values-night/splash.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/splash.xml b/app/src/main/res/values/splash.xml new file mode 100644 index 0000000..b637479 --- /dev/null +++ b/app/src/main/res/values/splash.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 6a842e5..30979df 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,6 @@ - \ No newline at end of file From f63aeaf6fa3ffaaa884d07b67094d00b8db1beca Mon Sep 17 00:00:00 2001 From: ibrahimcanerdogan Date: Fri, 17 May 2024 16:01:50 +0300 Subject: [PATCH 2/5] API Key local.properties added. --- .idea/deploymentTargetSelector.xml | 10 + .idea/gradle.xml | 5 +- .idea/migrations.xml | 10 + .idea/other.xml | 263 ++++++++++++++++++ app/build.gradle | 6 + .../com/loc/newsapp/data/remote/NewsApi.kt | 6 +- gradle.properties | 3 +- 7 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/other.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2..0897082 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..0d3a1fb --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a45378b..9201990 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,11 @@ android { vectorDrawables { useSupportLibrary true } + + Properties properties = new Properties() + properties.load(project.rootProject.file("local.properties").newDataInputStream()) + + buildConfigField("String","API_KEY",properties.getProperty("API_KEY")) } buildTypes { @@ -38,6 +43,7 @@ android { } buildFeatures { compose true + buildConfig true } composeOptions { kotlinCompilerExtensionVersion = "1.4.7" diff --git a/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt b/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt index 9c59345..40a17d1 100644 --- a/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt +++ b/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt @@ -1,7 +1,7 @@ package com.loc.newsapp.data.remote +import com.loc.newsapp.BuildConfig import com.loc.newsapp.data.remote.dto.NewsResponse -import com.loc.newsapp.util.Constants.API_KEY import retrofit2.http.GET import retrofit2.http.Query @@ -11,7 +11,7 @@ interface NewsApi { suspend fun getNews( @Query("page") page: Int, @Query("sources") sources: String, - @Query("apiKey") apiKey: String = API_KEY + @Query("apiKey") apiKey: String = BuildConfig.API_KEY ): NewsResponse @GET("everything") @@ -19,7 +19,7 @@ interface NewsApi { @Query("q") searchQuery: String, @Query("page") page: Int, @Query("sources") sources: String, - @Query("apiKey") apiKey: String = API_KEY + @Query("apiKey") apiKey: String = BuildConfig.API_KEY ): NewsResponse } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031e..6920c05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file From 90b52161d9db665fb8f0508eedf08954103a9c92 Mon Sep 17 00:00:00 2001 From: ibrahimcanerdogan Date: Fri, 17 May 2024 16:49:37 +0300 Subject: [PATCH 3/5] Package structure redesigned. --- app/build.gradle | 3 + .../main/java/com/loc/newsapp/MainActivity.kt | 16 +-- .../java/com/loc/newsapp/MainViewModel.kt | 8 +- .../java/com/loc/newsapp/NewsApplication.kt | 3 +- .../data/{local => database}/NewsDao.kt | 10 +- .../data/{local => database}/NewsDatabase.kt | 4 +- .../{local => database}/NewsTypeConvertor.kt | 2 +- .../LocalUserManagerImpl.kt} | 9 +- .../{remote/NewsApi.kt => network/NewsAPI.kt} | 6 +- .../{remote => network}/dto/NewsResponse.kt | 2 +- .../paging}/NewsPagingSource.kt | 5 +- .../paging}/SearchNewsPagingSource.kt | 5 +- .../data/repository/NewsRepositoryImpl.kt | 32 +++--- .../dependencyinjection/DatabaseModule.kt | 39 +++++++ .../dependencyinjection/ManagerModule.kt | 35 ++++++ .../dependencyinjection/NetworkModule.kt | 27 +++++ .../dependencyinjection/RepositoryModule.kt | 23 ++++ .../dependencyinjection/UseCaseModule.kt | 38 +++++++ .../main/java/com/loc/newsapp/di/AppModule.kt | 107 ------------------ .../LocalUserManager.kt} | 4 +- .../domain/repository/NewsRepository.kt | 13 ++- .../usecases/app_entry/AppEntryUseCases.kt | 6 - .../domain/usecases/app_entry/ReadAppEntry.kt | 14 --- .../domain/usecases/app_entry/SaveAppEntry.kt | 13 --- .../usecases/manager/AppEntryUseCase.kt | 6 + .../domain/usecases/manager/ReadAppEntry.kt | 14 +++ .../domain/usecases/manager/SaveAppEntry.kt | 13 +++ .../domain/usecases/news/DeleteArticle.kt | 3 +- .../newsapp/domain/usecases/news/GetNews.kt | 2 +- .../news/{NewsUseCases.kt => NewsUseCase.kt} | 2 +- .../domain/usecases/news/SearchNews.kt | 2 +- .../domain/usecases/news/SelectArticle.kt | 3 +- .../domain/usecases/news/SelectArticles.kt | 3 +- .../domain/usecases/news/UpsertArticle.kt | 3 +- .../component}/common/ArticleCard.kt | 10 +- .../component}/common/ArticlesList.kt | 6 +- .../component}/common/EmptyScreen.kt | 3 +- .../component}/common/NewsButton.kt | 2 +- .../component}/common/SearchBar.kt | 4 +- .../component}/common/ShimmerEffect.kt | 16 +-- .../component/detail}/DetailsTopBar.kt | 3 +- .../component/onboarding}/OnBoardingPage.kt | 10 +- .../component/onboarding}/PageIndicator.kt | 4 +- .../navigation}/NewsNavigator.kt | 26 ++--- .../components/NewsBottomNavigation.kt | 10 +- .../navigation/graph}/NavGraph.kt | 10 +- .../nvgraph => ui/navigation/graph}/Route.kt | 2 +- .../screen}/bookmark/BookmarkScreen.kt | 7 +- .../screen}/bookmark/BookmarkState.kt | 2 +- .../screen}/bookmark/BookmarkViewModel.kt | 8 +- .../screen}/details/DetailsEvent.kt | 2 +- .../screen}/details/DetailsScreen.kt | 8 +- .../screen}/details/DetailsViewModel.kt | 12 +- .../screen}/home/HomeEvent.kt | 2 +- .../screen}/home/HomeScreen.kt | 15 +-- .../screen}/home/HomeState.kt | 2 +- .../screen}/home/HomeViewModel.kt | 10 +- .../screen}/onboarding/OnBoardingEvent.kt | 2 +- .../screen}/onboarding/OnBoardingScreen.kt | 15 ++- .../screen}/onboarding/OnBoardingViewModel.kt | 8 +- .../screen}/onboarding/Page.kt | 2 +- .../screen}/search/SearchEvent.kt | 2 +- .../screen}/search/SearchScreen.kt | 9 +- .../screen}/search/SearchState.kt | 2 +- .../screen}/search/SearchViewModel.kt | 9 +- .../java/com/loc/newsapp/util/Constants.kt | 7 -- .../newsapp/{presentation => util}/Dimens.kt | 2 +- 67 files changed, 360 insertions(+), 347 deletions(-) rename app/src/main/java/com/loc/newsapp/data/{local => database}/NewsDao.kt (59%) rename app/src/main/java/com/loc/newsapp/data/{local => database}/NewsDatabase.kt (75%) rename app/src/main/java/com/loc/newsapp/data/{local => database}/NewsTypeConvertor.kt (92%) rename app/src/main/java/com/loc/newsapp/data/{manger/LocalUserMangerImpl.kt => manager/LocalUserManagerImpl.kt} (87%) rename app/src/main/java/com/loc/newsapp/data/{remote/NewsApi.kt => network/NewsAPI.kt} (83%) rename app/src/main/java/com/loc/newsapp/data/{remote => network}/dto/NewsResponse.kt (79%) rename app/src/main/java/com/loc/newsapp/data/{remote => network/paging}/NewsPagingSource.kt (91%) rename app/src/main/java/com/loc/newsapp/data/{remote => network/paging}/SearchNewsPagingSource.kt (91%) create mode 100644 app/src/main/java/com/loc/newsapp/dependencyinjection/DatabaseModule.kt create mode 100644 app/src/main/java/com/loc/newsapp/dependencyinjection/ManagerModule.kt create mode 100644 app/src/main/java/com/loc/newsapp/dependencyinjection/NetworkModule.kt create mode 100644 app/src/main/java/com/loc/newsapp/dependencyinjection/RepositoryModule.kt create mode 100644 app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt delete mode 100644 app/src/main/java/com/loc/newsapp/di/AppModule.kt rename app/src/main/java/com/loc/newsapp/domain/{manger/LocalUserManger.kt => manager/LocalUserManager.kt} (61%) delete mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt delete mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt delete mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/manager/AppEntryUseCase.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/manager/ReadAppEntry.kt create mode 100644 app/src/main/java/com/loc/newsapp/domain/usecases/manager/SaveAppEntry.kt rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{NewsUseCases.kt => NewsUseCase.kt} (91%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/ArticleCard.kt (93%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/ArticlesList.kt (94%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/EmptyScreen.kt (97%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/NewsButton.kt (96%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/SearchBar.kt (97%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/component}/common/ShimmerEffect.kt (84%) rename app/src/main/java/com/loc/newsapp/{presentation/details/components => ui/component/detail}/DetailsTopBar.kt (96%) rename app/src/main/java/com/loc/newsapp/{presentation/onboarding/components => ui/component/onboarding}/OnBoardingPage.kt (88%) rename app/src/main/java/com/loc/newsapp/{presentation/onboarding/components => ui/component/onboarding}/PageIndicator.kt (90%) rename app/src/main/java/com/loc/newsapp/{presentation/news_navigator => ui/navigation}/NewsNavigator.kt (89%) rename app/src/main/java/com/loc/newsapp/{presentation/news_navigator => ui/navigation}/components/NewsBottomNavigation.kt (92%) rename app/src/main/java/com/loc/newsapp/{presentation/nvgraph => ui/navigation/graph}/NavGraph.kt (76%) rename app/src/main/java/com/loc/newsapp/{presentation/nvgraph => ui/navigation/graph}/Route.kt (92%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/bookmark/BookmarkScreen.kt (86%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/bookmark/BookmarkState.kt (72%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/bookmark/BookmarkViewModel.kt (78%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/details/DetailsEvent.kt (81%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/details/DetailsScreen.kt (95%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/details/DetailsViewModel.kt (78%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/home/HomeEvent.kt (79%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/home/HomeScreen.kt (88%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/home/HomeState.kt (67%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/home/HomeViewModel.kt (79%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/onboarding/OnBoardingEvent.kt (61%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/onboarding/OnBoardingScreen.kt (88%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/onboarding/OnBoardingViewModel.kt (71%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/onboarding/Page.kt (94%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/search/SearchEvent.kt (76%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/search/SearchScreen.kt (83%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/search/SearchState.kt (84%) rename app/src/main/java/com/loc/newsapp/{presentation => ui/screen}/search/SearchViewModel.kt (80%) rename app/src/main/java/com/loc/newsapp/{presentation => util}/Dimens.kt (91%) diff --git a/app/build.gradle b/app/build.gradle index 9201990..d6ca32d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,9 @@ android { properties.load(project.rootProject.file("local.properties").newDataInputStream()) buildConfigField("String","API_KEY",properties.getProperty("API_KEY")) + buildConfigField("String","NEWS_DATABASE_NAME",properties.getProperty("NEWS_DATABASE_NAME")) + buildConfigField("String","BASE_URL",properties.getProperty("BASE_URL")) + } buildTypes { diff --git a/app/src/main/java/com/loc/newsapp/MainActivity.kt b/app/src/main/java/com/loc/newsapp/MainActivity.kt index 520cd07..54c567d 100644 --- a/app/src/main/java/com/loc/newsapp/MainActivity.kt +++ b/app/src/main/java/com/loc/newsapp/MainActivity.kt @@ -13,31 +13,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.loc.newsapp.data.local.NewsDao -import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.domain.model.Source -import com.loc.newsapp.presentation.nvgraph.NavGraph +import com.loc.newsapp.ui.navigation.graph.NavGraph import com.loc.newsapp.ui.theme.NewsAppTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - val viewModel by viewModels() + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) - - installSplashScreen().apply { setKeepOnScreenCondition { viewModel.splashCondition } } + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) setContent { NewsAppTheme { diff --git a/app/src/main/java/com/loc/newsapp/MainViewModel.kt b/app/src/main/java/com/loc/newsapp/MainViewModel.kt index ef7df62..b132a5d 100644 --- a/app/src/main/java/com/loc/newsapp/MainViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/MainViewModel.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases -import com.loc.newsapp.presentation.nvgraph.Route +import com.loc.newsapp.domain.usecases.manager.AppEntryUseCase +import com.loc.newsapp.ui.navigation.graph.Route import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.onEach import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val appEntryUseCases: AppEntryUseCases + private val appEntryUseCase: AppEntryUseCase ) : ViewModel() { var splashCondition by mutableStateOf(true) @@ -24,7 +24,7 @@ class MainViewModel @Inject constructor( private set init { - appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> + appEntryUseCase.readAppEntry().onEach { shouldStartFromHomeScreen -> if(shouldStartFromHomeScreen){ startDestination = Route.NewsNavigation.route }else{ diff --git a/app/src/main/java/com/loc/newsapp/NewsApplication.kt b/app/src/main/java/com/loc/newsapp/NewsApplication.kt index 3aa9a6a..0198b49 100644 --- a/app/src/main/java/com/loc/newsapp/NewsApplication.kt +++ b/app/src/main/java/com/loc/newsapp/NewsApplication.kt @@ -4,5 +4,4 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class NewsApplication: Application() { -} \ No newline at end of file +class NewsApplication: Application() \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt b/app/src/main/java/com/loc/newsapp/data/database/NewsDao.kt similarity index 59% rename from app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt rename to app/src/main/java/com/loc/newsapp/data/database/NewsDao.kt index 0d4e311..d781849 100644 --- a/app/src/main/java/com/loc/newsapp/data/local/NewsDao.kt +++ b/app/src/main/java/com/loc/newsapp/data/database/NewsDao.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.data.local +package com.loc.newsapp.data.database import androidx.room.Dao import androidx.room.Delete @@ -12,14 +12,14 @@ import kotlinx.coroutines.flow.Flow interface NewsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(article: Article) + suspend fun upsertArticleToDatabase(article: Article) @Delete - suspend fun delete(article: Article) + suspend fun deleteArticleFromDatabase(article: Article) @Query("SELECT * FROM Article") - fun getArticles(): Flow> + fun getAllArticlesFromDatabase(): Flow> @Query("SELECT * FROM Article WHERE url=:url") - suspend fun getArticle(url: String): Article? + suspend fun getSelectedArticleFromDatabase(url: String): Article? } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt b/app/src/main/java/com/loc/newsapp/data/database/NewsDatabase.kt similarity index 75% rename from app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt rename to app/src/main/java/com/loc/newsapp/data/database/NewsDatabase.kt index e4af0a2..5c56ec8 100644 --- a/app/src/main/java/com/loc/newsapp/data/local/NewsDatabase.kt +++ b/app/src/main/java/com/loc/newsapp/data/database/NewsDatabase.kt @@ -1,11 +1,11 @@ -package com.loc.newsapp.data.local +package com.loc.newsapp.data.database import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.loc.newsapp.domain.model.Article -@Database(entities = [Article::class], version = 2) +@Database(entities = [Article::class], version = 3) @TypeConverters(NewsTypeConvertor::class) abstract class NewsDatabase: RoomDatabase() { diff --git a/app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt b/app/src/main/java/com/loc/newsapp/data/database/NewsTypeConvertor.kt similarity index 92% rename from app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt rename to app/src/main/java/com/loc/newsapp/data/database/NewsTypeConvertor.kt index 6e5a4e2..a930b6e 100644 --- a/app/src/main/java/com/loc/newsapp/data/local/NewsTypeConvertor.kt +++ b/app/src/main/java/com/loc/newsapp/data/database/NewsTypeConvertor.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.data.local +package com.loc.newsapp.data.database import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter diff --git a/app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt b/app/src/main/java/com/loc/newsapp/data/manager/LocalUserManagerImpl.kt similarity index 87% rename from app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt rename to app/src/main/java/com/loc/newsapp/data/manager/LocalUserManagerImpl.kt index 52efad7..df53b67 100644 --- a/app/src/main/java/com/loc/newsapp/data/manger/LocalUserMangerImpl.kt +++ b/app/src/main/java/com/loc/newsapp/data/manager/LocalUserManagerImpl.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.data.manger +package com.loc.newsapp.data.manager import android.content.Context import androidx.datastore.core.DataStore @@ -6,15 +6,16 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import com.loc.newsapp.domain.manger.LocalUserManger +import com.loc.newsapp.domain.manager.LocalUserManager import com.loc.newsapp.util.Constants import com.loc.newsapp.util.Constants.USER_SETTINGS import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class LocalUserMangerImpl( +class LocalUserManagerImpl( private val context: Context -) : LocalUserManger { +) : LocalUserManager { + override suspend fun saveAppEntry() { context.dataStore.edit { settings -> settings[PreferencesKeys.APP_ENTRY] = true diff --git a/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt b/app/src/main/java/com/loc/newsapp/data/network/NewsAPI.kt similarity index 83% rename from app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt rename to app/src/main/java/com/loc/newsapp/data/network/NewsAPI.kt index 40a17d1..94d3ae6 100644 --- a/app/src/main/java/com/loc/newsapp/data/remote/NewsApi.kt +++ b/app/src/main/java/com/loc/newsapp/data/network/NewsAPI.kt @@ -1,11 +1,11 @@ -package com.loc.newsapp.data.remote +package com.loc.newsapp.data.network import com.loc.newsapp.BuildConfig -import com.loc.newsapp.data.remote.dto.NewsResponse +import com.loc.newsapp.data.network.dto.NewsResponse import retrofit2.http.GET import retrofit2.http.Query -interface NewsApi { +interface NewsAPI { @GET("everything") suspend fun getNews( diff --git a/app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt b/app/src/main/java/com/loc/newsapp/data/network/dto/NewsResponse.kt similarity index 79% rename from app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt rename to app/src/main/java/com/loc/newsapp/data/network/dto/NewsResponse.kt index 1f19985..ac1590d 100644 --- a/app/src/main/java/com/loc/newsapp/data/remote/dto/NewsResponse.kt +++ b/app/src/main/java/com/loc/newsapp/data/network/dto/NewsResponse.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.data.remote.dto +package com.loc.newsapp.data.network.dto import com.loc.newsapp.domain.model.Article diff --git a/app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt b/app/src/main/java/com/loc/newsapp/data/network/paging/NewsPagingSource.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt rename to app/src/main/java/com/loc/newsapp/data/network/paging/NewsPagingSource.kt index 23e0dff..12c9ecb 100644 --- a/app/src/main/java/com/loc/newsapp/data/remote/NewsPagingSource.kt +++ b/app/src/main/java/com/loc/newsapp/data/network/paging/NewsPagingSource.kt @@ -1,11 +1,12 @@ -package com.loc.newsapp.data.remote +package com.loc.newsapp.data.network.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.loc.newsapp.data.network.NewsAPI import com.loc.newsapp.domain.model.Article class NewsPagingSource( - private val newsApi: NewsApi, + private val newsApi: NewsAPI, private val sources: String ) : PagingSource() { diff --git a/app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt b/app/src/main/java/com/loc/newsapp/data/network/paging/SearchNewsPagingSource.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt rename to app/src/main/java/com/loc/newsapp/data/network/paging/SearchNewsPagingSource.kt index e43d71b..978aef6 100644 --- a/app/src/main/java/com/loc/newsapp/data/remote/SearchNewsPagingSource.kt +++ b/app/src/main/java/com/loc/newsapp/data/network/paging/SearchNewsPagingSource.kt @@ -1,11 +1,12 @@ -package com.loc.newsapp.data.remote +package com.loc.newsapp.data.network.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.loc.newsapp.data.network.NewsAPI import com.loc.newsapp.domain.model.Article class SearchNewsPagingSource( - private val newsApi: NewsApi, + private val newsApi: NewsAPI, private val searchQuery: String, private val sources: String ) : PagingSource() { diff --git a/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt b/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt index f2b0e47..6928053 100644 --- a/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt +++ b/app/src/main/java/com/loc/newsapp/data/repository/NewsRepositoryImpl.kt @@ -1,24 +1,22 @@ package com.loc.newsapp.data.repository -import android.util.Log import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import com.loc.newsapp.data.local.NewsDao -import com.loc.newsapp.data.remote.NewsApi -import com.loc.newsapp.data.remote.NewsPagingSource -import com.loc.newsapp.data.remote.SearchNewsPagingSource +import com.loc.newsapp.data.database.NewsDao +import com.loc.newsapp.data.network.NewsAPI +import com.loc.newsapp.data.network.paging.NewsPagingSource +import com.loc.newsapp.data.network.paging.SearchNewsPagingSource import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.onEach class NewsRepositoryImpl( - private val newsApi: NewsApi, + private val newsApi: NewsAPI, private val newsDao: NewsDao ) : NewsRepository { - override fun getNews(sources: List): Flow> { + override fun getNewsFromRepository(sources: List): Flow> { return Pager( config = PagingConfig(pageSize = 10), pagingSourceFactory = { @@ -30,7 +28,7 @@ class NewsRepositoryImpl( ).flow } - override fun searchNews(searchQuery: String, sources: List): Flow> { + override fun searchNewsFromRepository(searchQuery: String, sources: List): Flow> { return Pager( config = PagingConfig(pageSize = 10), pagingSourceFactory = { @@ -43,19 +41,19 @@ class NewsRepositoryImpl( ).flow } - override suspend fun upsertArticle(article: Article) { - newsDao.upsert(article) + override suspend fun upsertArticleRepository(article: Article) { + newsDao.upsertArticleToDatabase(article) } - override suspend fun deleteArticle(article: Article) { - newsDao.delete(article) + override suspend fun deleteArticleRepository(article: Article) { + newsDao.deleteArticleFromDatabase(article) } - override fun selectArticles(): Flow> { - return newsDao.getArticles() + override fun selectBookmarkArticlesRepository(): Flow> { + return newsDao.getAllArticlesFromDatabase() } - override suspend fun selectArticle(url: String): Article? { - return newsDao.getArticle(url) + override suspend fun selectBookmarkArticleRepository(url: String): Article? { + return newsDao.getSelectedArticleFromDatabase(url) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/DatabaseModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/DatabaseModule.kt new file mode 100644 index 0000000..bdf2aa4 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/DatabaseModule.kt @@ -0,0 +1,39 @@ +package com.loc.newsapp.dependencyinjection + +import android.app.Application +import androidx.room.Room +import com.loc.newsapp.BuildConfig +import com.loc.newsapp.data.database.NewsDao +import com.loc.newsapp.data.database.NewsDatabase +import com.loc.newsapp.data.database.NewsTypeConvertor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideNewsDatabase( + application: Application + ): NewsDatabase { + return Room.databaseBuilder( + context = application, + klass = NewsDatabase::class.java, + name = BuildConfig.NEWS_DATABASE_NAME + ).addTypeConverter(NewsTypeConvertor()) + .fallbackToDestructiveMigration() + .build() + } + + @Provides + @Singleton + fun provideNewsDao( + newsDatabase: NewsDatabase + ): NewsDao = newsDatabase.newsDao + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/ManagerModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/ManagerModule.kt new file mode 100644 index 0000000..0223ddc --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/ManagerModule.kt @@ -0,0 +1,35 @@ +package com.loc.newsapp.dependencyinjection + +import android.app.Application +import com.loc.newsapp.data.manager.LocalUserManagerImpl +import com.loc.newsapp.domain.manager.LocalUserManager +import com.loc.newsapp.domain.usecases.manager.AppEntryUseCase +import com.loc.newsapp.domain.usecases.manager.ReadAppEntry +import com.loc.newsapp.domain.usecases.manager.SaveAppEntry +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ManagerModule { + + @Provides + @Singleton + fun provideLocalUserManger( + application: Application + ): LocalUserManager = LocalUserManagerImpl(application) + + + @Provides + @Singleton + fun provideAppEntryUseCases( + localUserManager: LocalUserManager + ) = AppEntryUseCase( + readAppEntry = ReadAppEntry(localUserManager), + saveAppEntry = SaveAppEntry(localUserManager) + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/NetworkModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/NetworkModule.kt new file mode 100644 index 0000000..ab9a6b2 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/NetworkModule.kt @@ -0,0 +1,27 @@ +package com.loc.newsapp.dependencyinjection + +import com.loc.newsapp.BuildConfig +import com.loc.newsapp.data.network.NewsAPI +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideNewsApi(): NewsAPI { + return Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(NewsAPI::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/RepositoryModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/RepositoryModule.kt new file mode 100644 index 0000000..c3316db --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/RepositoryModule.kt @@ -0,0 +1,23 @@ +package com.loc.newsapp.dependencyinjection + +import com.loc.newsapp.data.database.NewsDao +import com.loc.newsapp.data.network.NewsAPI +import com.loc.newsapp.data.repository.NewsRepositoryImpl +import com.loc.newsapp.domain.repository.NewsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + + @Provides + @Singleton + fun provideNewsRepository( + newsApi: NewsAPI, + newsDao: NewsDao + ): NewsRepository = NewsRepositoryImpl(newsApi,newsDao) +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt new file mode 100644 index 0000000..589e349 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt @@ -0,0 +1,38 @@ +package com.loc.newsapp.dependencyinjection + +import com.loc.newsapp.data.database.NewsDao +import com.loc.newsapp.domain.repository.NewsRepository +import com.loc.newsapp.domain.usecases.news.DeleteArticle +import com.loc.newsapp.domain.usecases.news.GetNews +import com.loc.newsapp.domain.usecases.news.NewsUseCase +import com.loc.newsapp.domain.usecases.news.SearchNews +import com.loc.newsapp.domain.usecases.news.SelectArticle +import com.loc.newsapp.domain.usecases.news.SelectArticles +import com.loc.newsapp.domain.usecases.news.UpsertArticle +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object UseCaseModule { + + @Provides + @Singleton + fun provideNewsUseCases( + newsRepository: NewsRepository, + newsDao: NewsDao + ): NewsUseCase { + return NewsUseCase( + getNews = GetNews(newsRepository), + searchNews = SearchNews(newsRepository), + upsertArticle = UpsertArticle(newsRepository), + deleteArticle = DeleteArticle(newsRepository), + selectArticles = SelectArticles(newsRepository), + selectArticle = SelectArticle(newsRepository) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/di/AppModule.kt b/app/src/main/java/com/loc/newsapp/di/AppModule.kt deleted file mode 100644 index d2e9386..0000000 --- a/app/src/main/java/com/loc/newsapp/di/AppModule.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.loc.newsapp.di - -import android.app.Application -import androidx.room.Room -import com.loc.newsapp.data.local.NewsDao -import com.loc.newsapp.data.local.NewsDatabase -import com.loc.newsapp.data.local.NewsTypeConvertor -import com.loc.newsapp.data.manger.LocalUserMangerImpl -import com.loc.newsapp.data.remote.NewsApi -import com.loc.newsapp.data.repository.NewsRepositoryImpl -import com.loc.newsapp.domain.manger.LocalUserManger -import com.loc.newsapp.domain.repository.NewsRepository -import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases -import com.loc.newsapp.domain.usecases.app_entry.ReadAppEntry -import com.loc.newsapp.domain.usecases.app_entry.SaveAppEntry -import com.loc.newsapp.domain.usecases.news.DeleteArticle -import com.loc.newsapp.domain.usecases.news.GetNews -import com.loc.newsapp.domain.usecases.news.NewsUseCases -import com.loc.newsapp.domain.usecases.news.SearchNews -import com.loc.newsapp.domain.usecases.news.SelectArticle -import com.loc.newsapp.domain.usecases.news.SelectArticles -import com.loc.newsapp.domain.usecases.news.UpsertArticle -import com.loc.newsapp.util.Constants.BASE_URL -import com.loc.newsapp.util.Constants.NEWS_DATABASE_NAME -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides - @Singleton - fun provideLocalUserManger( - application: Application - ): LocalUserManger = LocalUserMangerImpl(application) - - - @Provides - @Singleton - fun provideAppEntryUseCases( - localUserManger: LocalUserManger - ) = AppEntryUseCases( - readAppEntry = ReadAppEntry(localUserManger), - saveAppEntry = SaveAppEntry(localUserManger) - ) - - @Provides - @Singleton - fun provideNewsApi(): NewsApi { - return Retrofit.Builder() - .baseUrl(BASE_URL) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(NewsApi::class.java) - } - - @Provides - @Singleton - fun provideNewsRepository( - newsApi: NewsApi, - newsDao: NewsDao - ): NewsRepository = NewsRepositoryImpl(newsApi,newsDao) - - @Provides - @Singleton - fun provideNewsUseCases( - newsRepository: NewsRepository, - newsDao: NewsDao - ): NewsUseCases { - return NewsUseCases( - getNews = GetNews(newsRepository), - searchNews = SearchNews(newsRepository), - upsertArticle = UpsertArticle(newsRepository), - deleteArticle = DeleteArticle(newsRepository), - selectArticles = SelectArticles(newsRepository), - selectArticle = SelectArticle(newsRepository) - ) - } - - @Provides - @Singleton - fun provideNewsDatabase( - application: Application - ): NewsDatabase { - return Room.databaseBuilder( - context = application, - klass = NewsDatabase::class.java, - name = NEWS_DATABASE_NAME - ).addTypeConverter(NewsTypeConvertor()) - .fallbackToDestructiveMigration() - .build() - } - - @Provides - @Singleton - fun provideNewsDao( - newsDatabase: NewsDatabase - ): NewsDao = newsDatabase.newsDao - - -} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt b/app/src/main/java/com/loc/newsapp/domain/manager/LocalUserManager.kt similarity index 61% rename from app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt rename to app/src/main/java/com/loc/newsapp/domain/manager/LocalUserManager.kt index 9577349..a10cdc6 100644 --- a/app/src/main/java/com/loc/newsapp/domain/manger/LocalUserManger.kt +++ b/app/src/main/java/com/loc/newsapp/domain/manager/LocalUserManager.kt @@ -1,8 +1,8 @@ -package com.loc.newsapp.domain.manger +package com.loc.newsapp.domain.manager import kotlinx.coroutines.flow.Flow -interface LocalUserManger { +interface LocalUserManager { suspend fun saveAppEntry() diff --git a/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt b/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt index cfcee45..b88b1ce 100644 --- a/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt +++ b/app/src/main/java/com/loc/newsapp/domain/repository/NewsRepository.kt @@ -6,15 +6,16 @@ import kotlinx.coroutines.flow.Flow interface NewsRepository { - fun getNews(sources: List): Flow> - fun searchNews(searchQuery: String,sources: List): Flow> + fun getNewsFromRepository(sources: List): Flow> - suspend fun upsertArticle(article: Article) + fun searchNewsFromRepository(searchQuery: String, sources: List): Flow> - suspend fun deleteArticle(article: Article) + suspend fun upsertArticleRepository(article: Article) - fun selectArticles(): Flow> + suspend fun deleteArticleRepository(article: Article) - suspend fun selectArticle(url: String): Article? + fun selectBookmarkArticlesRepository(): Flow> + + suspend fun selectBookmarkArticleRepository(url: String): Article? } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt deleted file mode 100644 index 2a850bd..0000000 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/AppEntryUseCases.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.loc.newsapp.domain.usecases.app_entry - -data class AppEntryUseCases( - val readAppEntry: ReadAppEntry, - val saveAppEntry: SaveAppEntry -) diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt deleted file mode 100644 index 8dc3a36..0000000 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/ReadAppEntry.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.loc.newsapp.domain.usecases.app_entry - -import com.loc.newsapp.domain.manger.LocalUserManger -import kotlinx.coroutines.flow.Flow - -class ReadAppEntry( - private val localUserManger: LocalUserManger -) { - - operator fun invoke(): Flow{ - return localUserManger.readAppEntry() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt deleted file mode 100644 index f8a09a8..0000000 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/app_entry/SaveAppEntry.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.loc.newsapp.domain.usecases.app_entry - -import com.loc.newsapp.domain.manger.LocalUserManger - -class SaveAppEntry( - private val localUserManger: LocalUserManger -) { - - suspend operator fun invoke(){ - localUserManger.saveAppEntry() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/manager/AppEntryUseCase.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/AppEntryUseCase.kt new file mode 100644 index 0000000..d1f1f05 --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/AppEntryUseCase.kt @@ -0,0 +1,6 @@ +package com.loc.newsapp.domain.usecases.manager + +data class AppEntryUseCase( + val readAppEntry: ReadAppEntry, + val saveAppEntry: SaveAppEntry +) diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/manager/ReadAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/ReadAppEntry.kt new file mode 100644 index 0000000..941044c --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/ReadAppEntry.kt @@ -0,0 +1,14 @@ +package com.loc.newsapp.domain.usecases.manager + +import com.loc.newsapp.domain.manager.LocalUserManager +import kotlinx.coroutines.flow.Flow + +class ReadAppEntry( + private val localUserManager: LocalUserManager +) { + + operator fun invoke(): Flow{ + return localUserManager.readAppEntry() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/manager/SaveAppEntry.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/SaveAppEntry.kt new file mode 100644 index 0000000..c1f62fa --- /dev/null +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/manager/SaveAppEntry.kt @@ -0,0 +1,13 @@ +package com.loc.newsapp.domain.usecases.manager + +import com.loc.newsapp.domain.manager.LocalUserManager + +class SaveAppEntry( + private val localUserManager: LocalUserManager +) { + + suspend operator fun invoke(){ + localUserManager.saveAppEntry() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt index f992ab5..08f54b4 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt @@ -1,6 +1,5 @@ package com.loc.newsapp.domain.usecases.news -import com.loc.newsapp.data.local.NewsDao import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository @@ -9,7 +8,7 @@ class DeleteArticle( ) { suspend operator fun invoke(article: Article){ - newsRepository.deleteArticle(article) + newsRepository.deleteArticleRepository(article) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt index 921d704..525889e 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt @@ -10,7 +10,7 @@ class GetNews( ) { operator fun invoke(sources: List): Flow>{ - return newsRepository.getNews(sources = sources) + return newsRepository.getNewsFromRepository(sources = sources) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt index 4876ef6..2d5f478 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCases.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt @@ -1,6 +1,6 @@ package com.loc.newsapp.domain.usecases.news -data class NewsUseCases( +data class NewsUseCase( val getNews: GetNews, val searchNews: SearchNews, val upsertArticle: UpsertArticle, diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt index 7f3e08e..57edd85 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt @@ -10,7 +10,7 @@ class SearchNews( ) { operator fun invoke(searchQuery: String,sources: List): Flow> { - return newsRepository.searchNews(searchQuery = searchQuery,sources = sources) + return newsRepository.searchNewsFromRepository(searchQuery = searchQuery,sources = sources) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt index 46d0b54..39e3599 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt @@ -1,6 +1,5 @@ package com.loc.newsapp.domain.usecases.news -import com.loc.newsapp.data.local.NewsDao import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository @@ -9,7 +8,7 @@ class SelectArticle( ) { suspend operator fun invoke(url: String): Article?{ - return newsRepository.selectArticle(url) + return newsRepository.selectBookmarkArticleRepository(url) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt index 561496e..b519d30 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt @@ -1,6 +1,5 @@ package com.loc.newsapp.domain.usecases.news -import com.loc.newsapp.data.local.NewsDao import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow @@ -10,7 +9,7 @@ class SelectArticles( ) { operator fun invoke(): Flow>{ - return newsRepository.selectArticles() + return newsRepository.selectBookmarkArticlesRepository() } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt index 8a79bac..a384e8b 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt @@ -1,6 +1,5 @@ package com.loc.newsapp.domain.usecases.news -import com.loc.newsapp.data.local.NewsDao import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository @@ -9,7 +8,7 @@ class UpsertArticle( ) { suspend operator fun invoke(article: Article){ - newsRepository.upsertArticle(article) + newsRepository.upsertArticleRepository(article) } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt similarity index 93% rename from app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt index 306aa28..704ec89 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/ArticleCard.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.clickable @@ -29,10 +29,10 @@ import coil.request.ImageRequest import com.loc.newsapp.R import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.model.Source -import com.loc.newsapp.presentation.Dimens.ArticleCardSize -import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding -import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 -import com.loc.newsapp.presentation.Dimens.SmallIconSize +import com.loc.newsapp.util.Dimens.ArticleCardSize +import com.loc.newsapp.util.Dimens.ExtraSmallPadding +import com.loc.newsapp.util.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.util.Dimens.SmallIconSize import com.loc.newsapp.ui.theme.NewsAppTheme @Composable diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticlesList.kt similarity index 94% rename from app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/ArticlesList.kt index 893f520..f48187d 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/ArticlesList.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticlesList.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -11,8 +11,8 @@ import androidx.compose.ui.Modifier import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 -import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.util.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.util.Dimens.MediumPadding1 @Composable fun ArticlesList( diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/EmptyScreen.kt similarity index 97% rename from app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/EmptyScreen.kt index 2b0c9a9..b28be04 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/EmptyScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/EmptyScreen.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.core.animateFloatAsState @@ -23,7 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color.Companion.DarkGray import androidx.compose.ui.graphics.Color.Companion.LightGray -import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/NewsButton.kt similarity index 96% rename from app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/NewsButton.kt index 28fec76..44b683a 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/NewsButton.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/NewsButton.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/SearchBar.kt similarity index 97% rename from app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/SearchBar.kt index faab865..f30901e 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/SearchBar.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/SearchBar.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.border @@ -28,7 +28,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.loc.newsapp.R -import com.loc.newsapp.presentation.Dimens.IconSize +import com.loc.newsapp.util.Dimens.IconSize import com.loc.newsapp.ui.theme.NewsAppTheme @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt similarity index 84% rename from app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt rename to app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt index d6ef47c..f821157 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/common/ShimmerEffect.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.common +package com.loc.newsapp.ui.component.common import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.core.RepeatMode @@ -7,36 +7,26 @@ 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.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.loc.newsapp.R -import com.loc.newsapp.presentation.Dimens -import com.loc.newsapp.presentation.Dimens.MediumPadding1 +import com.loc.newsapp.util.Dimens +import com.loc.newsapp.util.Dimens.MediumPadding1 import com.loc.newsapp.ui.theme.NewsAppTheme fun Modifier.shimmerEffect() = composed { diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt similarity index 96% rename from app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt rename to app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt index e29779f..7c17ab2 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/details/components/DetailsTopBar.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt @@ -1,11 +1,10 @@ -package com.loc.newsapp.presentation.details.components +package com.loc.newsapp.ui.component.detail import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt similarity index 88% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt rename to app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt index dabdf34..fba26ed 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/OnBoardingPage.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.onboarding.components +package com.loc.newsapp.ui.component.onboarding import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.Image @@ -18,10 +18,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import com.loc.newsapp.R -import com.loc.newsapp.presentation.Dimens.MediumPadding1 -import com.loc.newsapp.presentation.Dimens.MediumPadding2 -import com.loc.newsapp.presentation.onboarding.Page -import com.loc.newsapp.presentation.onboarding.pages +import com.loc.newsapp.util.Dimens.MediumPadding1 +import com.loc.newsapp.util.Dimens.MediumPadding2 +import com.loc.newsapp.ui.screen.onboarding.Page +import com.loc.newsapp.ui.screen.onboarding.pages import com.loc.newsapp.ui.theme.NewsAppTheme @Composable diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/PageIndicator.kt similarity index 90% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt rename to app/src/main/java/com/loc/newsapp/ui/component/onboarding/PageIndicator.kt index cd44957..99ff44b 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/components/PageIndicator.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/PageIndicator.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.onboarding.components +package com.loc.newsapp.ui.component.onboarding import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import com.loc.newsapp.presentation.Dimens.IndicatorSize +import com.loc.newsapp.util.Dimens.IndicatorSize import com.loc.newsapp.ui.theme.BlueGray @Composable diff --git a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt similarity index 89% rename from app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt rename to app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt index abaee2a..c2f0fdb 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/NewsNavigator.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.news_navigator +package com.loc.newsapp.ui.navigation import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize @@ -22,18 +22,18 @@ import androidx.navigation.compose.rememberNavController import androidx.paging.compose.collectAsLazyPagingItems import com.loc.newsapp.R import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.presentation.bookmark.BookmarkScreen -import com.loc.newsapp.presentation.bookmark.BookmarkViewModel -import com.loc.newsapp.presentation.details.DetailsEvent -import com.loc.newsapp.presentation.details.DetailsScreen -import com.loc.newsapp.presentation.details.DetailsViewModel -import com.loc.newsapp.presentation.home.HomeScreen -import com.loc.newsapp.presentation.home.HomeViewModel -import com.loc.newsapp.presentation.news_navigator.components.BottomNavigationItem -import com.loc.newsapp.presentation.news_navigator.components.NewsBottomNavigation -import com.loc.newsapp.presentation.nvgraph.Route -import com.loc.newsapp.presentation.search.SearchScreen -import com.loc.newsapp.presentation.search.SearchViewModel +import com.loc.newsapp.ui.screen.bookmark.BookmarkScreen +import com.loc.newsapp.ui.screen.bookmark.BookmarkViewModel +import com.loc.newsapp.ui.screen.details.DetailsEvent +import com.loc.newsapp.ui.screen.details.DetailsScreen +import com.loc.newsapp.ui.screen.details.DetailsViewModel +import com.loc.newsapp.ui.screen.home.HomeScreen +import com.loc.newsapp.ui.screen.home.HomeViewModel +import com.loc.newsapp.ui.navigation.components.BottomNavigationItem +import com.loc.newsapp.ui.navigation.components.NewsBottomNavigation +import com.loc.newsapp.ui.navigation.graph.Route +import com.loc.newsapp.ui.screen.search.SearchScreen +import com.loc.newsapp.ui.screen.search.SearchViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/components/NewsBottomNavigation.kt similarity index 92% rename from app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt rename to app/src/main/java/com/loc/newsapp/ui/navigation/components/NewsBottomNavigation.kt index 51ff854..8f3aeb2 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/news_navigator/components/NewsBottomNavigation.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/components/NewsBottomNavigation.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.news_navigator.components +package com.loc.newsapp.ui.navigation.components import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.annotation.DrawableRes @@ -14,7 +14,6 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource @@ -22,8 +21,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.loc.newsapp.R -import com.loc.newsapp.presentation.Dimens.ExtraSmallPadding2 -import com.loc.newsapp.presentation.Dimens.IconSize +import com.loc.newsapp.util.Dimens.ExtraSmallPadding2 +import com.loc.newsapp.util.Dimens.IconSize import com.loc.newsapp.ui.theme.NewsAppTheme @Composable @@ -78,7 +77,8 @@ fun NewsBottomNavigationPreview() { items = listOf( BottomNavigationItem(icon = R.drawable.ic_home, text = "Home"), BottomNavigationItem(icon = R.drawable.ic_search, text = "Search"), - BottomNavigationItem(icon = R.drawable.ic_bookmark, text = "Bookmark")), + BottomNavigationItem(icon = R.drawable.ic_bookmark, text = "Bookmark") + ), selected = 0, onItemClick = {} ) diff --git a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/graph/NavGraph.kt similarity index 76% rename from app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt rename to app/src/main/java/com/loc/newsapp/ui/navigation/graph/NavGraph.kt index 68a4629..398a17a 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/NavGraph.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/graph/NavGraph.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.nvgraph +package com.loc.newsapp.ui.navigation.graph import androidx.compose.runtime.Composable import androidx.hilt.navigation.compose.hiltViewModel @@ -6,11 +6,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navigation -import com.loc.newsapp.presentation.bookmark.BookmarkScreen -import com.loc.newsapp.presentation.bookmark.BookmarkViewModel -import com.loc.newsapp.presentation.news_navigator.NewsNavigator -import com.loc.newsapp.presentation.onboarding.OnBoardingScreen -import com.loc.newsapp.presentation.onboarding.OnBoardingViewModel +import com.loc.newsapp.ui.navigation.NewsNavigator +import com.loc.newsapp.ui.screen.onboarding.OnBoardingScreen +import com.loc.newsapp.ui.screen.onboarding.OnBoardingViewModel @Composable fun NavGraph( diff --git a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/graph/Route.kt similarity index 92% rename from app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt rename to app/src/main/java/com/loc/newsapp/ui/navigation/graph/Route.kt index 9f2da37..456c118 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/nvgraph/Route.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/graph/Route.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.nvgraph +package com.loc.newsapp.ui.navigation.graph sealed class Route( val route: String diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkScreen.kt similarity index 86% rename from app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkScreen.kt index 3146131..cf285cf 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkScreen.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.bookmark +package com.loc.newsapp.ui.screen.bookmark import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -14,9 +14,8 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.font.FontWeight import com.loc.newsapp.R import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.presentation.Dimens.MediumPadding1 -import com.loc.newsapp.presentation.common.ArticlesList -import com.loc.newsapp.presentation.nvgraph.Route +import com.loc.newsapp.util.Dimens.MediumPadding1 +import com.loc.newsapp.ui.component.common.ArticlesList @Composable fun BookmarkScreen( diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkState.kt similarity index 72% rename from app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkState.kt index 8571f11..739c449 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkState.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkState.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.bookmark +package com.loc.newsapp.ui.screen.bookmark import com.loc.newsapp.domain.model.Article diff --git a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt similarity index 78% rename from app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt index 6687889..7c8f56b 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/bookmark/BookmarkViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt @@ -1,10 +1,10 @@ -package com.loc.newsapp.presentation.bookmark +package com.loc.newsapp.ui.screen.bookmark import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.loc.newsapp.domain.usecases.news.NewsUseCases +import com.loc.newsapp.domain.usecases.news.NewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -12,7 +12,7 @@ import javax.inject.Inject @HiltViewModel class BookmarkViewModel @Inject constructor( - private val newsUseCases: NewsUseCases + private val newsUseCase: NewsUseCase ) : ViewModel() { @@ -24,7 +24,7 @@ class BookmarkViewModel @Inject constructor( } private fun getArticles() { - newsUseCases.selectArticles().onEach { + newsUseCase.selectArticles().onEach { _state.value = _state.value.copy(articles = it.asReversed()) }.launchIn(viewModelScope) } diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsEvent.kt similarity index 81% rename from app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsEvent.kt index 056919f..908845b 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsEvent.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsEvent.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.details +package com.loc.newsapp.ui.screen.details import com.loc.newsapp.domain.model.Article diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt similarity index 95% rename from app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt index 71fcd94..ac1008b 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.details +package com.loc.newsapp.ui.screen.details import android.content.Intent import android.net.Uri @@ -24,9 +24,9 @@ import coil.request.ImageRequest import com.loc.newsapp.R import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.model.Source -import com.loc.newsapp.presentation.Dimens.ArticleImageHeight -import com.loc.newsapp.presentation.Dimens.MediumPadding1 -import com.loc.newsapp.presentation.details.components.DetailsTopBar +import com.loc.newsapp.util.Dimens.ArticleImageHeight +import com.loc.newsapp.util.Dimens.MediumPadding1 +import com.loc.newsapp.ui.component.detail.DetailsTopBar import com.loc.newsapp.ui.theme.NewsAppTheme @Composable diff --git a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt similarity index 78% rename from app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt index 038e3dd..a626ed9 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/details/DetailsViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.details +package com.loc.newsapp.ui.screen.details import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -6,14 +6,14 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.domain.usecases.news.NewsUseCases +import com.loc.newsapp.domain.usecases.news.NewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DetailsViewModel @Inject constructor( - private val newsUseCases: NewsUseCases + private val newsUseCase: NewsUseCase ) : ViewModel() { var sideEffect by mutableStateOf(null) @@ -23,7 +23,7 @@ class DetailsViewModel @Inject constructor( when (event) { is DetailsEvent.UpsertDeleteArticle -> { viewModelScope.launch { - val article = newsUseCases.selectArticle(event.article.url) + val article = newsUseCase.selectArticle(event.article.url) if (article == null) { upsertArticle(event.article) } else { @@ -39,12 +39,12 @@ class DetailsViewModel @Inject constructor( } private suspend fun deleteArticle(article: Article) { - newsUseCases.deleteArticle(article = article) + newsUseCase.deleteArticle(article = article) sideEffect = "Article Deleted" } private suspend fun upsertArticle(article: Article) { - newsUseCases.upsertArticle(article = article) + newsUseCase.upsertArticle(article = article) sideEffect = "Article Saved" } diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeEvent.kt similarity index 79% rename from app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/home/HomeEvent.kt index 3037ffc..7750cef 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/home/HomeEvent.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeEvent.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.home +package com.loc.newsapp.ui.screen.home sealed class HomeEvent { data class UpdateScrollValue(val newValue: Int): HomeEvent() diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt similarity index 88% rename from app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt index 72d3646..359b399 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/home/HomeScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt @@ -1,14 +1,12 @@ -package com.loc.newsapp.presentation.home +package com.loc.newsapp.ui.screen.home import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,8 +14,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,15 +26,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.loc.newsapp.R -import com.loc.newsapp.presentation.Dimens.MediumPadding1 -import com.loc.newsapp.presentation.common.ArticlesList -import com.loc.newsapp.presentation.common.SearchBar -import com.loc.newsapp.presentation.nvgraph.Route -import com.loc.newsapp.ui.theme.NewsAppTheme +import com.loc.newsapp.util.Dimens.MediumPadding1 +import com.loc.newsapp.ui.component.common.ArticlesList +import com.loc.newsapp.ui.component.common.SearchBar import kotlinx.coroutines.delay @OptIn(ExperimentalFoundationApi::class) diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeState.kt similarity index 67% rename from app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/home/HomeState.kt index 20833dc..4ec280f 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/home/HomeState.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeState.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.home +package com.loc.newsapp.ui.screen.home data class HomeState( val scrollValue: Int = 0, diff --git a/app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt similarity index 79% rename from app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt index 5ad026b..dc290df 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt @@ -1,25 +1,23 @@ -package com.loc.newsapp.presentation.home +package com.loc.newsapp.ui.screen.home import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn -import com.loc.newsapp.domain.usecases.news.NewsUseCases +import com.loc.newsapp.domain.usecases.news.NewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val newsUseCases: NewsUseCases + private val newsUseCase: NewsUseCase ) : ViewModel() { private val _state = mutableStateOf(HomeState()) val state: State = _state - val news = newsUseCases.getNews( + val news = newsUseCase.getNews( sources = listOf("bbc-news", "abc-news", "al-jazeera-english") ).cachedIn(viewModelScope) fun onEvent(event: HomeEvent){ diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingEvent.kt similarity index 61% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingEvent.kt index c845e90..af42b98 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingEvent.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingEvent.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.onboarding +package com.loc.newsapp.ui.screen.onboarding sealed class OnBoardingEvent { diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingScreen.kt similarity index 88% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingScreen.kt index 2d315f0..eb8c656 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingScreen.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.onboarding +package com.loc.newsapp.ui.screen.onboarding import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement @@ -18,13 +18,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.loc.newsapp.presentation.Dimens.MediumPadding2 -import com.loc.newsapp.presentation.Dimens.PageIndicatorWidth -import com.loc.newsapp.presentation.common.NewsButton -import com.loc.newsapp.presentation.common.NewsTextButton -import com.loc.newsapp.presentation.onboarding.components.OnBoardingPage -import com.loc.newsapp.presentation.onboarding.components.PageIndicator +import com.loc.newsapp.util.Dimens.MediumPadding2 +import com.loc.newsapp.util.Dimens.PageIndicatorWidth +import com.loc.newsapp.ui.component.common.NewsButton +import com.loc.newsapp.ui.component.common.NewsTextButton +import com.loc.newsapp.ui.component.onboarding.OnBoardingPage +import com.loc.newsapp.ui.component.onboarding.PageIndicator import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingViewModel.kt similarity index 71% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingViewModel.kt index 98a38ea..57bd4e0 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/OnBoardingViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/OnBoardingViewModel.kt @@ -1,15 +1,15 @@ -package com.loc.newsapp.presentation.onboarding +package com.loc.newsapp.ui.screen.onboarding import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.loc.newsapp.domain.usecases.app_entry.AppEntryUseCases +import com.loc.newsapp.domain.usecases.manager.AppEntryUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class OnBoardingViewModel @Inject constructor( - private val appEntryUseCases: AppEntryUseCases + private val appEntryUseCase: AppEntryUseCase ): ViewModel() { @@ -23,7 +23,7 @@ class OnBoardingViewModel @Inject constructor( private fun saveAppEntry() { viewModelScope.launch { - appEntryUseCases.saveAppEntry() + appEntryUseCase.saveAppEntry() } } diff --git a/app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/Page.kt similarity index 94% rename from app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/onboarding/Page.kt index 0702fdc..43e5255 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/onboarding/Page.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/onboarding/Page.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.onboarding +package com.loc.newsapp.ui.screen.onboarding import androidx.annotation.DrawableRes import com.loc.newsapp.R diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchEvent.kt similarity index 76% rename from app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/search/SearchEvent.kt index 0c98548..1e700b7 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/search/SearchEvent.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchEvent.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.search +package com.loc.newsapp.ui.screen.search sealed class SearchEvent { diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchScreen.kt similarity index 83% rename from app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/search/SearchScreen.kt index ec85a23..bf1fd00 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/search/SearchScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchScreen.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.search +package com.loc.newsapp.ui.screen.search import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -10,10 +10,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.paging.compose.collectAsLazyPagingItems import com.loc.newsapp.domain.model.Article -import com.loc.newsapp.presentation.Dimens.MediumPadding1 -import com.loc.newsapp.presentation.common.ArticlesList -import com.loc.newsapp.presentation.common.SearchBar -import com.loc.newsapp.presentation.nvgraph.Route +import com.loc.newsapp.util.Dimens.MediumPadding1 +import com.loc.newsapp.ui.component.common.ArticlesList +import com.loc.newsapp.ui.component.common.SearchBar @Composable fun SearchScreen( diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchState.kt similarity index 84% rename from app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/search/SearchState.kt index 366af35..c00efc0 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/search/SearchState.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchState.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation.search +package com.loc.newsapp.ui.screen.search import androidx.paging.PagingData import com.loc.newsapp.domain.model.Article diff --git a/app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt similarity index 80% rename from app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt rename to app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt index 62de4e6..22a7695 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/search/SearchViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt @@ -1,18 +1,17 @@ -package com.loc.newsapp.presentation.search +package com.loc.newsapp.ui.screen.search import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn -import com.loc.newsapp.domain.usecases.news.NewsUseCases -import com.loc.newsapp.domain.usecases.news.SearchNews +import com.loc.newsapp.domain.usecases.news.NewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - private val newsUseCases: NewsUseCases + private val newsUseCase: NewsUseCase ) : ViewModel() { private val _state = mutableStateOf(SearchState()) @@ -31,7 +30,7 @@ class SearchViewModel @Inject constructor( } private fun searchNews() { - val articles = newsUseCases.searchNews( + val articles = newsUseCase.searchNews( searchQuery = state.value.searchQuery, sources = listOf("bbc-news", "abc-news", "al-jazeera-english") ).cachedIn(viewModelScope) diff --git a/app/src/main/java/com/loc/newsapp/util/Constants.kt b/app/src/main/java/com/loc/newsapp/util/Constants.kt index 1086178..9f40e2e 100644 --- a/app/src/main/java/com/loc/newsapp/util/Constants.kt +++ b/app/src/main/java/com/loc/newsapp/util/Constants.kt @@ -5,11 +5,4 @@ object Constants { const val USER_SETTINGS = "userSettings" const val APP_ENTRY = "appEntry" - - const val API_KEY = "fee38bf418b04948b7b372c045fdf499" - - const val BASE_URL = "https://newsapi.org/v2/" - - const val NEWS_DATABASE_NAME = "news_db" - } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/presentation/Dimens.kt b/app/src/main/java/com/loc/newsapp/util/Dimens.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/presentation/Dimens.kt rename to app/src/main/java/com/loc/newsapp/util/Dimens.kt index 5543421..5390aec 100644 --- a/app/src/main/java/com/loc/newsapp/presentation/Dimens.kt +++ b/app/src/main/java/com/loc/newsapp/util/Dimens.kt @@ -1,4 +1,4 @@ -package com.loc.newsapp.presentation +package com.loc.newsapp.util import androidx.compose.ui.unit.dp From 0205c48ea65107543ea99c2d21f1a57cb5d12226 Mon Sep 17 00:00:00 2001 From: ibrahimcanerdogan Date: Sat, 18 May 2024 00:05:00 +0300 Subject: [PATCH 4/5] Wrong codes changed. --- .../dependencyinjection/UseCaseModule.kt | 24 ++++---- ...eteArticle.kt => DeleteArticleDatabase.kt} | 2 +- .../news/{GetNews.kt => GetAllNews.kt} | 2 +- .../news/{SearchNews.kt => GetSearchNews.kt} | 2 +- .../domain/usecases/news/NewsUseCase.kt | 12 ++-- ...kt => SelectBookmarkAllArticleDatabase.kt} | 2 +- ...le.kt => SelectBookmarkArticleDatabase.kt} | 2 +- ...ertArticle.kt => UpsertArticleDatabase.kt} | 2 +- .../ui/component/common/ArticleCard.kt | 10 ++-- .../ui/component/common/ShimmerEffect.kt | 56 ++++++++----------- .../ui/component/detail/DetailsTopBar.kt | 3 +- .../ui/component/onboarding/OnBoardingPage.kt | 2 - .../newsapp/ui/navigation/NewsNavigator.kt | 10 +--- .../ui/screen/bookmark/BookmarkViewModel.kt | 2 +- .../ui/screen/details/DetailsViewModel.kt | 6 +- .../loc/newsapp/ui/screen/home/HomeScreen.kt | 1 - .../newsapp/ui/screen/home/HomeViewModel.kt | 2 +- .../ui/screen/search/SearchViewModel.kt | 2 +- 18 files changed, 61 insertions(+), 81 deletions(-) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{DeleteArticle.kt => DeleteArticleDatabase.kt} (91%) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{GetNews.kt => GetAllNews.kt} (95%) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{SearchNews.kt => GetSearchNews.kt} (95%) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{SelectArticles.kt => SelectBookmarkAllArticleDatabase.kt} (89%) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{SelectArticle.kt => SelectBookmarkArticleDatabase.kt} (89%) rename app/src/main/java/com/loc/newsapp/domain/usecases/news/{UpsertArticle.kt => UpsertArticleDatabase.kt} (91%) diff --git a/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt b/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt index 589e349..2b9c62f 100644 --- a/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt +++ b/app/src/main/java/com/loc/newsapp/dependencyinjection/UseCaseModule.kt @@ -2,13 +2,13 @@ package com.loc.newsapp.dependencyinjection import com.loc.newsapp.data.database.NewsDao import com.loc.newsapp.domain.repository.NewsRepository -import com.loc.newsapp.domain.usecases.news.DeleteArticle -import com.loc.newsapp.domain.usecases.news.GetNews +import com.loc.newsapp.domain.usecases.news.DeleteArticleDatabase +import com.loc.newsapp.domain.usecases.news.GetAllNews import com.loc.newsapp.domain.usecases.news.NewsUseCase -import com.loc.newsapp.domain.usecases.news.SearchNews -import com.loc.newsapp.domain.usecases.news.SelectArticle -import com.loc.newsapp.domain.usecases.news.SelectArticles -import com.loc.newsapp.domain.usecases.news.UpsertArticle +import com.loc.newsapp.domain.usecases.news.GetSearchNews +import com.loc.newsapp.domain.usecases.news.SelectBookmarkArticleDatabase +import com.loc.newsapp.domain.usecases.news.SelectBookmarkAllArticleDatabase +import com.loc.newsapp.domain.usecases.news.UpsertArticleDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -26,12 +26,12 @@ object UseCaseModule { newsDao: NewsDao ): NewsUseCase { return NewsUseCase( - getNews = GetNews(newsRepository), - searchNews = SearchNews(newsRepository), - upsertArticle = UpsertArticle(newsRepository), - deleteArticle = DeleteArticle(newsRepository), - selectArticles = SelectArticles(newsRepository), - selectArticle = SelectArticle(newsRepository) + getAllNews = GetAllNews(newsRepository), + getSearchNews = GetSearchNews(newsRepository), + upsertArticleDatabase = UpsertArticleDatabase(newsRepository), + deleteArticleDatabase = DeleteArticleDatabase(newsRepository), + selectBookmarkAllArticleDatabase = SelectBookmarkAllArticleDatabase(newsRepository), + selectBookmarkArticleDatabase = SelectBookmarkArticleDatabase(newsRepository) ) } diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticleDatabase.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticleDatabase.kt index 08f54b4..d413e6c 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/DeleteArticleDatabase.kt @@ -3,7 +3,7 @@ package com.loc.newsapp.domain.usecases.news import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository -class DeleteArticle( +class DeleteArticleDatabase( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetAllNews.kt similarity index 95% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/GetAllNews.kt index 525889e..4ce1846 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetNews.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetAllNews.kt @@ -5,7 +5,7 @@ import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow -class GetNews( +class GetAllNews( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetSearchNews.kt similarity index 95% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/GetSearchNews.kt index 57edd85..a615ac1 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SearchNews.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/GetSearchNews.kt @@ -5,7 +5,7 @@ import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow -class SearchNews( +class GetSearchNews( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt index 2d5f478..fb52bb8 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/NewsUseCase.kt @@ -1,10 +1,10 @@ package com.loc.newsapp.domain.usecases.news data class NewsUseCase( - val getNews: GetNews, - val searchNews: SearchNews, - val upsertArticle: UpsertArticle, - val deleteArticle: DeleteArticle, - val selectArticles: SelectArticles, - val selectArticle: SelectArticle + val getAllNews: GetAllNews, + val getSearchNews: GetSearchNews, + val upsertArticleDatabase: UpsertArticleDatabase, + val deleteArticleDatabase: DeleteArticleDatabase, + val selectBookmarkAllArticleDatabase: SelectBookmarkAllArticleDatabase, + val selectBookmarkArticleDatabase: SelectBookmarkArticleDatabase ) diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkAllArticleDatabase.kt similarity index 89% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkAllArticleDatabase.kt index b519d30..3546399 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticles.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkAllArticleDatabase.kt @@ -4,7 +4,7 @@ import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository import kotlinx.coroutines.flow.Flow -class SelectArticles( +class SelectBookmarkAllArticleDatabase( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkArticleDatabase.kt similarity index 89% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkArticleDatabase.kt index 39e3599..c340941 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/SelectBookmarkArticleDatabase.kt @@ -3,7 +3,7 @@ package com.loc.newsapp.domain.usecases.news import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository -class SelectArticle( +class SelectBookmarkArticleDatabase( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticleDatabase.kt similarity index 91% rename from app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt rename to app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticleDatabase.kt index a384e8b..12ac94b 100644 --- a/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticle.kt +++ b/app/src/main/java/com/loc/newsapp/domain/usecases/news/UpsertArticleDatabase.kt @@ -3,7 +3,7 @@ package com.loc.newsapp.domain.usecases.news import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.repository.NewsRepository -class UpsertArticle( +class UpsertArticleDatabase( private val newsRepository: NewsRepository ) { diff --git a/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt index 704ec89..4c2c4b2 100644 --- a/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/ArticleCard.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import com.loc.newsapp.R @@ -48,6 +49,7 @@ fun ArticleCard( AsyncImage( modifier = Modifier .size(ArticleCardSize) + .padding(5.dp) .clip(MaterialTheme.shapes.medium), model = ImageRequest.Builder(context).data(article.urlToImage).build(), contentDescription = null, @@ -58,16 +60,12 @@ fun ArticleCard( verticalArrangement = Arrangement.SpaceAround, modifier = Modifier .padding(horizontal = ExtraSmallPadding) - .height( - ArticleCardSize - ) + .height(ArticleCardSize) ) { Text( text = article.title, style = MaterialTheme.typography.bodyMedium, - color = colorResource( - id = R.color.text_title - ), + color = colorResource(id = R.color.text_title), maxLines = 2, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt b/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt index f821157..e35daf1 100644 --- a/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/common/ShimmerEffect.kt @@ -29,20 +29,6 @@ import com.loc.newsapp.util.Dimens import com.loc.newsapp.util.Dimens.MediumPadding1 import com.loc.newsapp.ui.theme.NewsAppTheme -fun Modifier.shimmerEffect() = composed { - val transition = rememberInfiniteTransition() - val alpha = transition.animateFloat( - initialValue = 0.2f, - targetValue = 0.9f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000), - repeatMode = RepeatMode.Reverse - ) - ).value - background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha)) -} - - @Composable fun ArticleCardShimmerEffect( modifier: Modifier = Modifier @@ -51,46 +37,48 @@ fun ArticleCardShimmerEffect( Row(modifier = modifier) { Box( - modifier = Modifier + modifier = shimmerEffect() .size(Dimens.ArticleCardSize) .clip(MaterialTheme.shapes.medium) - .shimmerEffect(), - - ) - + ) Column( verticalArrangement = Arrangement.SpaceAround, modifier = Modifier .padding(horizontal = Dimens.ExtraSmallPadding) - .height( - Dimens.ArticleCardSize - ) + .height(Dimens.ArticleCardSize) ) { Box( - modifier = Modifier + modifier = shimmerEffect() .fillMaxWidth() .height(30.dp) .padding(horizontal = MediumPadding1) - .shimmerEffect(), - - ) - + ) Row(verticalAlignment = Alignment.CenterVertically) { Box( - modifier = Modifier + modifier = shimmerEffect() .fillMaxWidth(0.5f) .height(15.dp) .padding(horizontal = MediumPadding1) - .shimmerEffect(), - - ) + ) } - } - } } +private fun shimmerEffect(): Modifier = Modifier.composed { + val transition = rememberInfiniteTransition(label = "Shimmer Effect") + val alpha = transition.animateFloat( + initialValue = 0.2f, + targetValue = 0.9f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1000), + repeatMode = RepeatMode.Reverse + ), + label = "Shimmer Effect" + ).value + background(color = colorResource(id = R.color.shimmer).copy(alpha = alpha)) +} + @Preview(showBackground = true) @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) @Composable @@ -98,4 +86,4 @@ fun ArticleCardShimmerEffectPreview() { NewsAppTheme { ArticleCardShimmerEffect() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt index 7c17ab2..7b20c33 100644 --- a/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -31,7 +32,7 @@ fun DetailsTopBar( ) { TopAppBar( - title = { }, + title = { Text(text = "") }, modifier = Modifier.fillMaxWidth(), colors = TopAppBarDefaults.mediumTopAppBarColors( containerColor = Color.Transparent, diff --git a/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt index fba26ed..9c83733 100644 --- a/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/onboarding/OnBoardingPage.kt @@ -29,7 +29,6 @@ fun OnBoardingPage( modifier: Modifier = Modifier, page: Page ) { - Column(modifier = modifier) { Image( modifier = Modifier @@ -53,7 +52,6 @@ fun OnBoardingPage( color = colorResource(id = R.color.text_medium) ) } - } diff --git a/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt index c2f0fdb..1ae4798 100644 --- a/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -49,9 +50,7 @@ fun NewsNavigator() { val navController = rememberNavController() val backstackState = navController.currentBackStackEntryAsState().value - var selectedItem by rememberSaveable { - mutableStateOf(0) - } + var selectedItem by rememberSaveable { mutableIntStateOf(0) } selectedItem = remember(key1 = backstackState) { when (backstackState?.destination?.route) { @@ -83,12 +82,10 @@ fun NewsNavigator() { navController = navController, route = Route.HomeScreen.route ) - 1 -> navigateToTap( navController = navController, route = Route.SearchScreen.route ) - 2 -> navigateToTap( navController = navController, route = Route.BookmarkScreen.route @@ -148,8 +145,7 @@ fun NewsNavigator() { composable(route = Route.DetailsScreen.route) { val viewModel: DetailsViewModel = hiltViewModel() if (viewModel.sideEffect != null) { - Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT) - .show() + Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT).show() viewModel.onEvent(DetailsEvent.RemoveSideEffect) } navController.previousBackStackEntry?.savedStateHandle?.get("article") diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt index 7c8f56b..8bf58fa 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/bookmark/BookmarkViewModel.kt @@ -24,7 +24,7 @@ class BookmarkViewModel @Inject constructor( } private fun getArticles() { - newsUseCase.selectArticles().onEach { + newsUseCase.selectBookmarkAllArticleDatabase().onEach { _state.value = _state.value.copy(articles = it.asReversed()) }.launchIn(viewModelScope) } diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt index a626ed9..510c353 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt @@ -23,7 +23,7 @@ class DetailsViewModel @Inject constructor( when (event) { is DetailsEvent.UpsertDeleteArticle -> { viewModelScope.launch { - val article = newsUseCase.selectArticle(event.article.url) + val article = newsUseCase.selectBookmarkArticleDatabase(event.article.url) if (article == null) { upsertArticle(event.article) } else { @@ -39,12 +39,12 @@ class DetailsViewModel @Inject constructor( } private suspend fun deleteArticle(article: Article) { - newsUseCase.deleteArticle(article = article) + newsUseCase.deleteArticleDatabase(article = article) sideEffect = "Article Deleted" } private suspend fun upsertArticle(article: Article) { - newsUseCase.upsertArticle(article = article) + newsUseCase.upsertArticleDatabase(article = article) sideEffect = "Article Saved" } diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt index 359b399..f4053ea 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeScreen.kt @@ -34,7 +34,6 @@ import com.loc.newsapp.ui.component.common.ArticlesList import com.loc.newsapp.ui.component.common.SearchBar import kotlinx.coroutines.delay -@OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( state: HomeState, diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt index dc290df..9bbd1a7 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/home/HomeViewModel.kt @@ -17,7 +17,7 @@ class HomeViewModel @Inject constructor( private val _state = mutableStateOf(HomeState()) val state: State = _state - val news = newsUseCase.getNews( + val news = newsUseCase.getAllNews( sources = listOf("bbc-news", "abc-news", "al-jazeera-english") ).cachedIn(viewModelScope) fun onEvent(event: HomeEvent){ diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt index 22a7695..7ce3e9a 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/search/SearchViewModel.kt @@ -30,7 +30,7 @@ class SearchViewModel @Inject constructor( } private fun searchNews() { - val articles = newsUseCase.searchNews( + val articles = newsUseCase.getSearchNews( searchQuery = state.value.searchQuery, sources = listOf("bbc-news", "abc-news", "al-jazeera-english") ).cachedIn(viewModelScope) From 8245c27304de16981b85e4fd289e43fc108db502 Mon Sep 17 00:00:00 2001 From: ibrahimcanerdogan Date: Sat, 18 May 2024 17:44:53 +0300 Subject: [PATCH 5/5] Bookmark icon change features added. --- .../ui/component/detail/DetailsTopBar.kt | 22 ++++++++++++++----- .../newsapp/ui/navigation/NewsNavigator.kt | 9 ++++---- .../ui/screen/details/DetailsScreen.kt | 18 ++++++++++----- .../ui/screen/details/DetailsViewModel.kt | 19 ++++++++++++---- .../main/java/com/loc/newsapp/util/Dimens.kt | 1 + app/src/main/res/drawable/ic_bookmark_add.xml | 9 ++++++++ .../main/res/drawable/ic_bookmark_remove.xml | 9 ++++++++ 7 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 app/src/main/res/drawable/ic_bookmark_add.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_remove.xml diff --git a/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt index 7b20c33..f5c7fb1 100644 --- a/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt +++ b/app/src/main/java/com/loc/newsapp/ui/component/detail/DetailsTopBar.kt @@ -14,23 +14,27 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.loc.newsapp.R +import com.loc.newsapp.domain.model.Article +import com.loc.newsapp.domain.model.Source import com.loc.newsapp.ui.theme.NewsAppTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun DetailsTopBar( + sideEffect: Boolean?, onBrowsingClick: () -> Unit, onShareClick: () -> Unit, onBookmarkClick: () -> Unit, onBackClick: () -> Unit ) { - TopAppBar( title = { Text(text = "") }, modifier = Modifier.fillMaxWidth(), @@ -49,10 +53,17 @@ fun DetailsTopBar( }, actions = { IconButton(onClick = onBookmarkClick) { - Icon( - painter = painterResource(id = R.drawable.ic_bookmark), - contentDescription = null - ) + if (sideEffect == true) { + Icon( + painter = painterResource(id = R.drawable.ic_bookmark_remove), + contentDescription = null + ) + } else { + Icon( + painter = painterResource(id = R.drawable.ic_bookmark_add), + contentDescription = null + ) + } } IconButton(onClick = onShareClick) { Icon( @@ -77,6 +88,7 @@ fun DetailsTopBarPreivew() { NewsAppTheme { Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { DetailsTopBar( + sideEffect = true, onBrowsingClick = { /*TODO*/ }, onShareClick = { /*TODO*/ }, onBookmarkClick = { /*TODO*/ }) { diff --git a/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt index 1ae4798..3c1a0d5 100644 --- a/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt +++ b/app/src/main/java/com/loc/newsapp/ui/navigation/NewsNavigator.kt @@ -144,15 +144,16 @@ fun NewsNavigator() { composable(route = Route.DetailsScreen.route) { val viewModel: DetailsViewModel = hiltViewModel() - if (viewModel.sideEffect != null) { - Toast.makeText(LocalContext.current, viewModel.sideEffect, Toast.LENGTH_SHORT).show() - viewModel.onEvent(DetailsEvent.RemoveSideEffect) - } + navController.previousBackStackEntry?.savedStateHandle?.get("article") ?.let { article -> DetailsScreen( article = article, + sideEffect = viewModel.sideEffect, event = viewModel::onEvent, + controlBookmark = { + viewModel.controlBookmarkedArticle(article) + }, navigateUp = { navController.navigateUp() }) } } diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt index ac1008b..e866ba2 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale @@ -32,18 +34,24 @@ import com.loc.newsapp.ui.theme.NewsAppTheme @Composable fun DetailsScreen( article: Article, + sideEffect: Boolean?, event: (DetailsEvent) -> Unit, + controlBookmark: () -> Unit, navigateUp: () -> Unit ) { - val context = LocalContext.current + LaunchedEffect(key1 = article) { + controlBookmark.invoke() + } + Column( modifier = Modifier .fillMaxSize() .statusBarsPadding() ) { DetailsTopBar( + sideEffect = sideEffect, onBrowsingClick = { Intent(Intent.ACTION_VIEW).also { it.data = Uri.parse(article.url) @@ -126,10 +134,10 @@ fun DetailsScreenPreview() { url = "https://consent.google.com/ml?continue=https://news.google.com/rss/articles/CBMiaWh0dHBzOi8vY3J5cHRvc2F1cnVzLnRlY2gvY29pbmJhc2Utc2F5cy1hcHBsZS1ibG9ja2VkLWl0cy1sYXN0LWFwcC1yZWxlYXNlLW9uLW5mdHMtaW4td2FsbGV0LXJldXRlcnMtY29tL9IBAA?oc%3D5&gl=FR&hl=en-US&cm=2&pc=n&src=1", urlToImage = "https://media.wired.com/photos/6495d5e893ba5cd8bbdc95af/191:100/w_1280,c_limit/The-EU-Rules-Phone-Batteries-Must-Be-Replaceable-Gear-2BE6PRN.jpg" ), - event = {} - ) { - - } + sideEffect = false, + event = {}, + controlBookmark = {} + ) {} } } diff --git a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt index 510c353..966f138 100644 --- a/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt +++ b/app/src/main/java/com/loc/newsapp/ui/screen/details/DetailsViewModel.kt @@ -8,6 +8,11 @@ import androidx.lifecycle.viewModelScope import com.loc.newsapp.domain.model.Article import com.loc.newsapp.domain.usecases.news.NewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -16,8 +21,7 @@ class DetailsViewModel @Inject constructor( private val newsUseCase: NewsUseCase ) : ViewModel() { - var sideEffect by mutableStateOf(null) - private set + var sideEffect by mutableStateOf(null) fun onEvent(event: DetailsEvent) { when (event) { @@ -40,12 +44,19 @@ class DetailsViewModel @Inject constructor( private suspend fun deleteArticle(article: Article) { newsUseCase.deleteArticleDatabase(article = article) - sideEffect = "Article Deleted" + sideEffect = false } private suspend fun upsertArticle(article: Article) { newsUseCase.upsertArticleDatabase(article = article) - sideEffect = "Article Saved" + sideEffect = true } + fun controlBookmarkedArticle(article: Article) { + viewModelScope.launch { + newsUseCase.selectBookmarkAllArticleDatabase().collect { + sideEffect = it.contains(article) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/loc/newsapp/util/Dimens.kt b/app/src/main/java/com/loc/newsapp/util/Dimens.kt index 5390aec..cd9f02a 100644 --- a/app/src/main/java/com/loc/newsapp/util/Dimens.kt +++ b/app/src/main/java/com/loc/newsapp/util/Dimens.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.unit.dp object Dimens { val MediumPadding1 = 24.dp + val MediumPadding2 = 30.dp val IndicatorSize = 14.dp diff --git a/app/src/main/res/drawable/ic_bookmark_add.xml b/app/src/main/res/drawable/ic_bookmark_add.xml new file mode 100644 index 0000000..a8ffca2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bookmark_remove.xml b/app/src/main/res/drawable/ic_bookmark_remove.xml new file mode 100644 index 0000000..8d29d4c --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_remove.xml @@ -0,0 +1,9 @@ + + +