diff --git a/app/build.gradle b/app/build.gradle index 75677b5..503ad62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,12 +5,12 @@ plugins { android { namespace 'com.lahsuak.apps.jetpackcomposebasic' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.lahsuak.apps.jetpackcomposebasic" minSdk 23 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0" @@ -27,17 +27,17 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } buildFeatures { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.1.1' + kotlinCompilerExtensionVersion '1.4.3' } packagingOptions { resources { @@ -48,16 +48,30 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' - implementation 'androidx.activity:activity-compose:1.6.1' + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.activity:activity-compose:1.7.2' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.compose.material3:material3:1.1.0-alpha03' + implementation 'androidx.compose.material3:material3:1.2.0-alpha03' + implementation 'androidx.paging:paging-common-ktx:3.2.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.4' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + + implementation "androidx.paging:paging-compose:3.2.0" + + implementation 'com.jsibbold:zoomage:1.3.1' + + implementation "androidx.navigation:navigation-compose:2.6.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" + //retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.squareup.retrofit2:converter-gson:2.9.0" + //coil + implementation("io.coil-kt:coil-compose:2.2.2") + implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ebfd06..2c643f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + ) diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/model/UrlModel.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/model/UrlModel.kt new file mode 100644 index 0000000..353e9d4 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/model/UrlModel.kt @@ -0,0 +1,3 @@ +package com.lahsuak.apps.jetpackcomposebasic.model + +data class UrlModel(val regular: String) \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiInstance.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiInstance.kt new file mode 100644 index 0000000..2c9fee3 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiInstance.kt @@ -0,0 +1,16 @@ +package com.lahsuak.apps.jetpackcomposebasic.network + +import com.lahsuak.apps.jetpackcomposebasic.util.AppConstants.BASE_URL +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class ApiInstance { + companion object { + fun getRetroInstance(): ApiService { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(ApiService::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiService.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiService.kt new file mode 100644 index 0000000..2bf8ab0 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/network/ApiService.kt @@ -0,0 +1,31 @@ +package com.lahsuak.apps.jetpackcomposebasic.network + +import com.lahsuak.apps.jetpackcomposebasic.util.AppConstants.API_KEY +import com.lahsuak.apps.jetpackcomposebasic.model.ImageModel +import com.lahsuak.apps.jetpackcomposebasic.model.SearchModel +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Query + +interface ApiService { + + @Headers( + "Accept: application/json", + "Authorization: Client-ID $API_KEY" + ) + @GET("/photos") + suspend fun getImages( + @Query("page") page: Int, + @Query("per_page") perPage: Int + ): Response> + + @Headers("Authorization: Client-ID $API_KEY") + @GET("/search/photos") + suspend fun getSearchImages( + @Query("query") query: String, + @Query("page") page: Int, + @Query("per_page") perPage: Int + ): List + +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navhost/AppNavHost.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navhost/AppNavHost.kt new file mode 100644 index 0000000..a79190c --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navhost/AppNavHost.kt @@ -0,0 +1,41 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.navhost + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.lahsuak.apps.jetpackcomposebasic.ui.navigation.NavigationItem +import com.lahsuak.apps.jetpackcomposebasic.ui.screen.HomeScreen +import com.lahsuak.apps.jetpackcomposebasic.ui.screen.ViewScreen +import com.lahsuak.apps.jetpackcomposebasic.ui.viewmodel.MainViewModel + +@Composable +fun AppNavHost( + modifier: Modifier = Modifier, + navController: NavHostController, + viewModel: MainViewModel, + startDestination: String = NavigationItem.Home.route, +) { + NavHost( + modifier = modifier, + navController = navController, + startDestination = startDestination + ) { + composable(NavigationItem.Home.route) { + HomeScreen(viewModel, navController) + } + composable("${NavigationItem.ViewWallpaper.route}/{url}", + arguments = listOf(navArgument("url") { + type = NavType.StringType + }) + ) { navBackStackEntry -> + val url = navBackStackEntry.arguments?.getString("url") + url?.let { + ViewScreen(it, navController) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navigation/NavigationItem.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navigation/NavigationItem.kt new file mode 100644 index 0000000..d840c5c --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/navigation/NavigationItem.kt @@ -0,0 +1,11 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.navigation + +enum class Screen { + HOME, + VIEW_WALLPAPER +} + +sealed class NavigationItem(val route: String) { + object Home : NavigationItem(Screen.HOME.name) + object ViewWallpaper : NavigationItem(Screen.VIEW_WALLPAPER.name) +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/HomeScreen.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/HomeScreen.kt new file mode 100644 index 0000000..cba6455 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/HomeScreen.kt @@ -0,0 +1,113 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.paging.LoadState +import coil.compose.AsyncImage +import com.lahsuak.apps.jetpackcomposebasic.ui.navigation.NavigationItem +import com.lahsuak.apps.jetpackcomposebasic.ui.viewmodel.MainViewModel +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen(viewModel: MainViewModel, navController: NavController) { + var query by rememberSaveable { + mutableStateOf("") + } + val data = viewModel.dataFlow.collectAsLazyPagingItems() + + Column(modifier = Modifier.fillMaxSize()) { + SearchBar( + query = query, + onQueryChange = { + query = it + }, + onSearch = {}, + active = false, + onActiveChange = { + + }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + placeholder = { + Text("Search wallpaper", color = Color.Gray) + }, leadingIcon = { + Icon(imageVector = Icons.Default.Search, contentDescription = "Search") + } + ) {} + + LazyVerticalGrid(columns = GridCells.Fixed(3)) { + if (data.loadState.refresh == LoadState.Loading) { + item { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } + } + + items(data.itemSnapshotList.items) { item -> + AsyncImage( + model = item.urls.regular, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(120.dp) + .clickable { + val encodedUrl = + URLEncoder.encode( + item.urls.regular, + StandardCharsets.UTF_8.toString() + ) + + navController.navigate("${NavigationItem.ViewWallpaper.route}/$encodedUrl") + } + ) + } + if (data.loadState.append == LoadState.Loading) { + item { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ImageItem.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ImageItem.kt new file mode 100644 index 0000000..b08309a --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ImageItem.kt @@ -0,0 +1,15 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.screen + +import androidx.compose.runtime.Composable +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage +import com.lahsuak.apps.jetpackcomposebasic.model.ImageModel + +@Composable +fun ImageItem(item: ImageModel) { + AsyncImage( + model = item.urls.regular, + contentDescription = null, + contentScale = ContentScale.Crop + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ViewScreen.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ViewScreen.kt new file mode 100644 index 0000000..b39c1da --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/screen/ViewScreen.kt @@ -0,0 +1,121 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.screen + +import android.annotation.SuppressLint +import android.app.WallpaperManager +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.widget.Toast +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import coil.ImageLoader +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.request.SuccessResult +import com.lahsuak.apps.jetpackcomposebasic.ui.theme.JetPackComposeBasicTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ViewScreen(imageUrl: String, navController: NavController) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + Box(Modifier.fillMaxSize()) { + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillBounds + ) + Row( + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + horizontalArrangement = Arrangement.Center + ) { + IconButton( + onClick = { navController.popBackStack() }, + colors = IconButtonColors( + containerColor = Color.Gray, + contentColor = Color.White, + disabledContainerColor = Color.Transparent, + disabledContentColor = Color.Black + ) + ) { + Icon(Icons.Default.ArrowBack, null) + } + OutlinedButton( + onClick = { + setWallpaper(context, imageUrl, coroutineScope) + }, + colors = ButtonColors( + containerColor = Color.Gray, + contentColor = Color.White, + disabledContainerColor = Color.Transparent, + disabledContentColor = Color.Black + ) + ) { + Text("Set Wallpaper", color = Color.White) + } + } + } +} + +private fun setWallpaper(context: Context, url: String, coroutineScope: CoroutineScope) { + coroutineScope.launch { + val wallpaperManager = WallpaperManager.getInstance(context) + + val loader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(url) + .allowHardware(false) // Disable hardware bitmaps. + .build() + + val result = (loader.execute(request) as SuccessResult).drawable + val image = (result as BitmapDrawable).bitmap + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + wallpaperManager.setBitmap(image, null, true, WallpaperManager.FLAG_SYSTEM) + wallpaperManager.setWallpaperOffsetSteps(1F, 1F) + } else { + wallpaperManager.setBitmap(image) + } + Toast.makeText(context, "Wallpaper set!", Toast.LENGTH_SHORT).show() + } +} + +@Preview(showBackground = true) +@Composable +fun ViewScreenPreview() { + JetPackComposeBasicTheme { + ViewScreen( + "", rememberNavController() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/viewmodel/MainViewModel.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..0f4e666 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/ui/viewmodel/MainViewModel.kt @@ -0,0 +1,35 @@ +package com.lahsuak.apps.jetpackcomposebasic.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.lahsuak.apps.jetpackcomposebasic.model.ImageModel +import com.lahsuak.apps.jetpackcomposebasic.network.ApiInstance +import com.lahsuak.apps.jetpackcomposebasic.util.ImagePagingSource +import com.lahsuak.apps.jetpackcomposebasic.network.ApiService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class MainViewModel : ViewModel() { + private val retroService: ApiService = ApiInstance.getRetroInstance() + private var currentSearch: String = "" + + val dataFlow = + Pager( + config = PagingConfig( + pageSize = 10, + maxSize = 30, + ), + pagingSourceFactory = { ImagePagingSource(retroService, "") } + ).flow.cachedIn(viewModelScope) + + fun getListData(query: String, isSearch: Boolean): Flow> { + return Pager(config = PagingConfig(pageSize = 10, maxSize = 30), + pagingSourceFactory = { ImagePagingSource(retroService, query) }).flow.cachedIn( + viewModelScope + ) + } +} diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/AppConstants.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/AppConstants.kt new file mode 100644 index 0000000..a6c249e --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/AppConstants.kt @@ -0,0 +1,7 @@ +package com.lahsuak.apps.jetpackcomposebasic.util + +object AppConstants { + const val BASE_URL = "https://api.unsplash.com/" + const val API_KEY = "ag9QdyIQv09QAlplEQrnZP0w4mhsQIlyKFgAGn-CO6w" + const val PAGE_COUNT =30 +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/ImagePagingSource.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/ImagePagingSource.kt new file mode 100644 index 0000000..46e542c --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/util/ImagePagingSource.kt @@ -0,0 +1,48 @@ +package com.lahsuak.apps.jetpackcomposebasic.util + + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.lahsuak.apps.jetpackcomposebasic.model.ImageModel +import com.lahsuak.apps.jetpackcomposebasic.util.AppConstants.PAGE_COUNT +import com.lahsuak.apps.jetpackcomposebasic.network.ApiService + +private const val TAG = "TAG" + +class ImagePagingSource( + private val apiService: ApiService, private val query: String, +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: FIRST_PAGE_INDEX + val data: List + if (query == "") { + val response = apiService.getImages(currentPage, PAGE_COUNT) + data = response.body() ?: emptyList() + } else { + val response = apiService.getSearchImages(query, currentPage, PAGE_COUNT) + data = response.first().results + Log.d(TAG, "load: inside ${data.size}") + } + val responseData = mutableListOf() + responseData.addAll(data) + Log.d(TAG, "load: ${data.size} $currentPage") + LoadResult.Page( + data = data, + prevKey = if (currentPage == 1) null else -1, + nextKey = currentPage.plus(1) + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + companion object { + private const val FIRST_PAGE_INDEX = 1 + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index e868fc8..b0f2f02 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ buildscript { ext { - compose_version = '1.3.2' + compose_version = '1.4.3' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.3.0' apply false - id 'com.android.library' version '7.3.0' apply false - id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id 'com.android.application' version '8.0.2' apply false + id 'com.android.library' version '8.0.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false } \ No newline at end of file