Realised receiving from API logic and album displaying
This commit is contained in:
parent
b45b1c6d18
commit
9ec4271c73
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
6
.idea/kotlinc.xml
Normal file
6
.idea/kotlinc.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -1,6 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
alias(libs.plugins.navigation.safe.args)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -30,6 +32,9 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
@ -42,11 +47,33 @@ dependencies {
|
|||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
|
// Architecture Components
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
|
||||||
|
|
||||||
|
// Retrofit for API
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
|
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
|
||||||
|
|
||||||
|
// Glide for image loading
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
|
|
||||||
|
// CardView and RecyclerView
|
||||||
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
|
|
||||||
|
// Shimmer for loading effect
|
||||||
|
implementation("com.facebook.shimmer:shimmer:0.5.0")
|
||||||
}
|
}
|
@ -2,6 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@ -17,7 +20,6 @@
|
|||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
package com.example.gallery
|
package com.example.gallery
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import com.example.gallery.databinding.ActivityMainBinding
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private lateinit var binding: ActivityMainBinding
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setContentView(R.layout.activity_main)
|
super.onCreate(savedInstanceState)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
setContentView(binding.root)
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
val navHostFragment = supportFragmentManager
|
||||||
|
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
|
val navController = navHostFragment.navController
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
setupActionBarWithNavController(navController)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
val navHostFragment = supportFragmentManager
|
||||||
|
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
|
val navController = navHostFragment.navController
|
||||||
|
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||||
|
}
|
||||||
|
}
|
12
app/src/main/java/com/example/gallery/models/Album.kt
Normal file
12
app/src/main/java/com/example/gallery/models/Album.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.gallery.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Album(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val imageUrl: String,
|
||||||
|
val photos: List<Photo>? = null
|
||||||
|
) : Parcelable
|
12
app/src/main/java/com/example/gallery/models/Photo.kt
Normal file
12
app/src/main/java/com/example/gallery/models/Photo.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.gallery.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Photo(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val imageUrl: String
|
||||||
|
) : Parcelable
|
13
app/src/main/java/com/example/gallery/remote/ApiService.kt
Normal file
13
app/src/main/java/com/example/gallery/remote/ApiService.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.example.gallery.remote
|
||||||
|
|
||||||
|
import com.example.gallery.models.Album
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface ApiService {
|
||||||
|
@GET("albums")
|
||||||
|
suspend fun getAlbums(): List<Album>
|
||||||
|
|
||||||
|
@GET("albums/{albumId}")
|
||||||
|
suspend fun getAlbumDetails(@Path("albumId") albumId: Int): Album
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.example.gallery.remote
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object RetrofitClient {
|
||||||
|
private const val BASE_URL = "https://gallery.fishrungames.com/"
|
||||||
|
|
||||||
|
private val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
private val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(loggingInterceptor)
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiService: ApiService by lazy {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.baseUrl(BASE_URL)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(ApiService::class.java)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.example.gallery.repository
|
||||||
|
|
||||||
|
import com.example.gallery.models.Album
|
||||||
|
import com.example.gallery.remote.RetrofitClient
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class GalleryRepository {
|
||||||
|
private val apiService = RetrofitClient.apiService
|
||||||
|
|
||||||
|
suspend fun getAlbums(): Result<List<Album>> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val response = apiService.getAlbums()
|
||||||
|
Result.success(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAlbumDetails(albumId: Int): Result<Album> = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val response = apiService.getAlbumDetails(albumId)
|
||||||
|
Result.success(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/com/example/gallery/ui/ImageLoader.kt
Normal file
17
app/src/main/java/com/example/gallery/ui/ImageLoader.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.example.gallery.ui
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
|
import com.example.gallery.R
|
||||||
|
|
||||||
|
object ImageLoader {
|
||||||
|
fun loadImage(imageView: ImageView, url: String) {
|
||||||
|
Glide.with(imageView.context)
|
||||||
|
.load(url)
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.error(R.drawable.error_image)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package com.example.gallery.ui.albums
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.gallery.databinding.ItemAlbumBinding
|
||||||
|
import com.example.gallery.models.Album
|
||||||
|
import com.example.gallery.ui.ImageLoader
|
||||||
|
|
||||||
|
class AlbumAdapter(private val onAlbumClick: (Album) -> Unit) :
|
||||||
|
ListAdapter<Album, AlbumAdapter.AlbumViewHolder>(AlbumDiffCallback()) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder {
|
||||||
|
val binding = ItemAlbumBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return AlbumViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AlbumViewHolder(private val binding: ItemAlbumBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
val position = bindingAdapterPosition
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
onAlbumClick(getItem(position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(album: Album) {
|
||||||
|
binding.albumTitle.text = album.title
|
||||||
|
ImageLoader.loadImage(binding.albumImage, album.imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AlbumDiffCallback : DiffUtil.ItemCallback<Album>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Album, newItem: Album): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Album, newItem: Album): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.example.gallery.ui.albums
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.example.gallery.databinding.FragmentAlbumsBinding
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
|
class AlbumsFragment : Fragment() {
|
||||||
|
private var _binding: FragmentAlbumsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val viewModel: AlbumsViewModel by viewModels()
|
||||||
|
private lateinit var albumAdapter: AlbumAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAlbumsBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
setupSwipeRefresh()
|
||||||
|
observeViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
albumAdapter = AlbumAdapter { album ->
|
||||||
|
val action = AlbumsFragmentDirections.actionAlbumsFragmentToPhotosFragment(album)
|
||||||
|
findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recyclerView.apply {
|
||||||
|
adapter = albumAdapter
|
||||||
|
layoutManager = GridLayoutManager(requireContext(), 2)
|
||||||
|
setHasFixedSize(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSwipeRefresh() {
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
viewModel.refreshAlbums()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewModel() {
|
||||||
|
viewModel.albums.observe(viewLifecycleOwner) { albums ->
|
||||||
|
albumAdapter.submitList(albums)
|
||||||
|
binding.emptyView.isVisible = albums.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = isLoading
|
||||||
|
binding.shimmerLayout.isVisible = isLoading && albumAdapter.itemCount == 0
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
binding.shimmerLayout.startShimmer()
|
||||||
|
} else {
|
||||||
|
binding.shimmerLayout.stopShimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.error.observe(viewLifecycleOwner) { errorMessage ->
|
||||||
|
if (!errorMessage.isNullOrEmpty()) {
|
||||||
|
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction("Повторить") {
|
||||||
|
viewModel.refreshAlbums()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package com.example.gallery.ui.albums
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.gallery.models.Album
|
||||||
|
import com.example.gallery.repository.GalleryRepository
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AlbumsViewModel : ViewModel() {
|
||||||
|
private val repository = GalleryRepository()
|
||||||
|
|
||||||
|
private val _albums = MutableLiveData<List<Album>>()
|
||||||
|
val albums: LiveData<List<Album>> = _albums
|
||||||
|
|
||||||
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
|
val isLoading: LiveData<Boolean> = _isLoading
|
||||||
|
|
||||||
|
private val _error = MutableLiveData<String?>()
|
||||||
|
val error: LiveData<String?> = _error
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadAlbums()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAlbums() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_error.value = null
|
||||||
|
|
||||||
|
repository.getAlbums().fold(
|
||||||
|
onSuccess = { albumsList ->
|
||||||
|
_albums.value = albumsList
|
||||||
|
_isLoading.value = false
|
||||||
|
},
|
||||||
|
onFailure = { e ->
|
||||||
|
_error.value = e.message ?: "Неизвестная ошибка"
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAlbums() {
|
||||||
|
loadAlbums()
|
||||||
|
}
|
||||||
|
}
|
6
app/src/main/res/drawable/error_image.xml
Normal file
6
app/src/main/res/drawable/error_image.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFCCCC" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
8
app/src/main/res/drawable/gradient_overlay.xml
Normal file
8
app/src/main/res/drawable/gradient_overlay.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<gradient
|
||||||
|
android:angle="90"
|
||||||
|
android:endColor="#00000000"
|
||||||
|
android:startColor="#88000000"
|
||||||
|
android:type="linear" />
|
||||||
|
</shape>
|
6
app/src/main/res/drawable/placeholder_image.xml
Normal file
6
app/src/main/res/drawable/placeholder_image.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#EEEEEE" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
@ -2,18 +2,35 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/appBarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Hello World!"
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:titleTextColor="@android:color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/nav_host_fragment"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:defaultNavHost="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
||||||
|
app:navGraph="@navigation/nav_graph" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
61
app/src/main/res/layout/fragment_albums.xml
Normal file
61
app/src/main/res/layout/fragment_albums.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.albums.AlbumsFragment">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="2"
|
||||||
|
tools:itemCount="6"
|
||||||
|
tools:listitem="@layout/item_album" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
|
android:id="@+id/shimmerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include layout="@layout/shimmer_album_item" />
|
||||||
|
<include layout="@layout/shimmer_album_item" />
|
||||||
|
<include layout="@layout/shimmer_album_item" />
|
||||||
|
<include layout="@layout/shimmer_album_item" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emptyView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="Нет доступных альбомов"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
54
app/src/main/res/layout/item_album.xml
Normal file
54
app/src/main/res/layout/item_album.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/albumImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="Обложка альбома"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/gradient_overlay"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/albumTitle" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/albumTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="Название альбома" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
37
app/src/main/res/layout/shimmer_album_item.xml
Normal file
37
app/src/main/res/layout/shimmer_album_item.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/shimmerImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="#DDDDDD"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/shimmerTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="#BBBBBB"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
40
app/src/main/res/navigation/nav_graph.xml
Normal file
40
app/src/main/res/navigation/nav_graph.xml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/nav_graph"
|
||||||
|
app:startDestination="@id/albumsFragment">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/albumsFragment"
|
||||||
|
android:name="com.example.gallery.ui.albums.AlbumsFragment"
|
||||||
|
android:label="Альбомы"
|
||||||
|
tools:layout="@layout/fragment_albums">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_albumsFragment_to_photosFragment"
|
||||||
|
app:destination="@id/photosFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/photosFragment"
|
||||||
|
android:name="com.example.gallery.ui.photos.PhotosFragment"
|
||||||
|
android:label="Фотографии"
|
||||||
|
tools:layout="@layout/fragment_photos">
|
||||||
|
<argument
|
||||||
|
android:name="album"
|
||||||
|
app:argType="com.example.gallery.data.model.Album" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_photosFragment_to_photoDetailFragment"
|
||||||
|
app:destination="@id/photoDetailFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/photoDetailFragment"
|
||||||
|
android:name="com.example.gallery.ui.photo_detail.PhotoDetailFragment"
|
||||||
|
android:label="Просмотр фото"
|
||||||
|
tools:layout="@layout/fragment_photo_detail">
|
||||||
|
<argument
|
||||||
|
android:name="photo"
|
||||||
|
app:argType="com.example.gallery.data.model.Photo" />
|
||||||
|
</fragment>
|
||||||
|
</navigation>
|
@ -1,6 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.8.0"
|
agp = "8.8.0"
|
||||||
kotlin = "1.9.24"
|
kotlin = "1.9.24"
|
||||||
|
navigation = "2.7.2"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
@ -9,6 +10,7 @@ appcompat = "1.7.0"
|
|||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
activity = "1.10.1"
|
activity = "1.10.1"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
|
swiperefreshlayout = "1.1.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@ -19,8 +21,10 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
|||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
navigation-safe-args = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user