Skip to content

Commit

Permalink
Migrate Favorites screen to Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
SIKV committed Jun 24, 2024
1 parent 5471a1d commit 4cd760d
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 341 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.sikv.photos.compose.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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

@Composable
fun NoContent(
title: String,
message: String? = null
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
) {
Text(title,
style = MaterialTheme.typography.titleLarge
)
message?.let {
Text(message,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
fun Scaffold(
title: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
content: @Composable BoxScope.() -> Unit
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Expand All @@ -31,6 +32,7 @@ fun Scaffold(
scrollBehavior = scrollBehavior
)
},
snackbarHost = snackbarHost,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
Expand Down
13 changes: 13 additions & 0 deletions feature/favorites/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ android {

buildFeatures {
viewBinding true
compose true
}

composeOptions {
kotlinCompilerExtensionVersion libs.versions.composeCompiler.get()
}
}

Expand All @@ -18,14 +23,22 @@ dependencies {
implementation project(':data')
implementation project(':common')
implementation project(':common-ui')
implementation project(':compose-ui')
implementation project(':photo-usecase')
implementation project(':navigation')
implementation project(':photo-list-ui')

implementation libs.material
implementation libs.androidx.fragment

implementation libs.androidx.compose.material3
implementation libs.accompanist.themeadapter.material3
implementation libs.androidx.lifecycle.viewmodel
implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.androidx.lifecycle.runtime.compose

implementation libs.inject
kapt libs.hilt.compiler
implementation libs.hilt.android
implementation libs.androidx.hilt.navigation.compose
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,210 +2,57 @@ package com.github.sikv.photos.favorites

import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.sikv.photo.list.ui.PhotoActionDispatcher
import com.github.sikv.photo.list.ui.PhotoItemLayoutType
import com.github.sikv.photo.list.ui.adapter.PhotoListAdapter
import com.github.sikv.photo.list.ui.setItemLayoutType
import com.github.sikv.photos.common.DownloadService
import com.github.sikv.photos.common.PhotoLoader
import com.github.sikv.photos.common.ui.BaseFragment
import com.github.sikv.photos.common.ui.scrollToTop
import com.github.sikv.photos.common.ui.setupToolbar
import com.github.sikv.photos.common.ui.toolbar.FragmentToolbar
import com.github.sikv.photos.data.repository.FavoritesRepository
import com.github.sikv.photos.domain.ListLayout
import com.github.sikv.photos.favorites.databinding.FragmentFavoritesBinding
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.github.sikv.photo.usecase.PhotoActionsUseCase
import com.github.sikv.photos.navigation.args.PhotoDetailsFragmentArguments
import com.github.sikv.photos.navigation.route.PhotoDetailsRoute
import com.github.sikv.photos.navigation.route.SetWallpaperRoute
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class FavoritesFragment : BaseFragment() {

@Inject
lateinit var favoritesRepository: FavoritesRepository

@Inject
lateinit var downloadService: DownloadService

@Inject
lateinit var photoLoader: PhotoLoader
class FavoritesFragment : Fragment() {

@Inject
lateinit var photoDetailsRoute: PhotoDetailsRoute

@Inject
lateinit var setWallpaperRoute: SetWallpaperRoute

private val viewModel: FavoritesViewModel by viewModels()

private val photoActionDispatcher by lazy {
PhotoActionDispatcher(
fragment = this,
downloadService = downloadService,
photoLoader = photoLoader,
photoDetailsRoute = photoDetailsRoute,
setWallpaperRoute = setWallpaperRoute,
onToggleFavorite = viewModel::toggleFavorite,
onShowMessage = ::showMessage
)
}

private lateinit var photoAdapter: PhotoListAdapter

private var removedSnackbar: Snackbar? = null

private var _binding: FragmentFavoritesBinding? = null
private val binding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

photoAdapter = PhotoListAdapter(
photoLoader = photoLoader,
favoritesRepository = favoritesRepository,
lifecycleScope = lifecycleScope,
listener = photoActionDispatcher
)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFavoritesBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

setupToolbar(R.string.favorites)

binding.photosRecycler.adapter = photoAdapter

// Default value is not working good. When a photo is removed animation is broken.
binding.photosRecycler.itemAnimator?.removeDuration = 0

collectUiState()
}

override fun onDestroyView() {
super.onDestroyView()

_binding = null
}

override fun onCreateToolbar(): FragmentToolbar {
return FragmentToolbar.Builder()
.withId(R.id.toolbar)
.withMenu(R.menu.menu_favorites)
.withMenuItems(
listOf(
R.id.itemViewList,
R.id.itemViewGrid,
R.id.itemSortBy,
R.id.itemRemoveAll
),
listOf(
object : MenuItem.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
viewModel.updateListLayout(ListLayout.LIST)
return true
}
},
lateinit var photoActionsUseCase: PhotoActionsUseCase

object : MenuItem.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
viewModel.updateListLayout(ListLayout.GRID)
return true
}
},

object : MenuItem.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
viewModel.createSortByDialog().show(childFragmentManager)
return true
}
},

object : MenuItem.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
viewModel.markAllAsRemoved()
return true
}
}
)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
.build()
}

override fun onScrollToTop() {
binding.photosRecycler.scrollToTop()
}

private fun collectUiState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
updateUiState(uiState)
}
}
}
}

private fun updateUiState(uiState: FavoritesUiState) {
when (uiState) {
is FavoritesUiState.Data -> {
photoAdapter.submitList(uiState.photos)
binding.noResultsView.isVisible = uiState.photos.isEmpty()

updateListLayout(uiState.listLayout)

if (uiState.shouldShowRemovedNotification) {
showFavoritesRemovedSnackbar()
setContent {
Mdc3Theme {
FavoritesScreen(
onPhotoClick = { photo ->
photoDetailsRoute.present(findNavController(), PhotoDetailsFragmentArguments(photo))
},
onPhotoAttributionClick = { photo ->
photoActionsUseCase.photoAttributionClick(photo)
},
onPhotoActionsClick = { photo ->
photoActionsUseCase.openMoreActions(requireNotNull(activity), photo)
},
onSharePhotoClick = { photo ->
photoActionsUseCase.sharePhoto(requireNotNull(activity), photo)
},
onDownloadPhotoClick = { photo ->
photoActionsUseCase.downloadPhoto(requireNotNull(activity), photo)
},
onShowDialog = { dialog ->
dialog.show(childFragmentManager, "Tag") // TODO: Add tag.
}
)
}
}
}
}

private fun showFavoritesRemovedSnackbar() {
if (removedSnackbar?.isShown == true) {
return
}
removedSnackbar = Snackbar.make(binding.root, R.string.removed, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) {
viewModel.unmarkAllAsRemoved()
}
.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
viewModel.removeAllMarked()
}
})

removedSnackbar?.show()
}

private fun updateListLayout(listLayout: ListLayout) {
val itemLayoutType = PhotoItemLayoutType.findBySpanCount(listLayout.spanCount)

photoAdapter.setItemLayoutType(itemLayoutType)
binding.photosRecycler.setItemLayoutType(itemLayoutType)

setMenuItemVisibility(R.id.itemViewList, listLayout == ListLayout.GRID)
setMenuItemVisibility(R.id.itemViewGrid, listLayout == ListLayout.LIST)
}
}
Loading

0 comments on commit 4cd760d

Please sign in to comment.