Skip to content

Commit

Permalink
✨ implement fragment layout for search screen
Browse files Browse the repository at this point in the history
  • Loading branch information
kmkim2689 committed Jul 25, 2024
1 parent 0c82aac commit 8796094
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package net.pengcook.android.data.datasource

import androidx.paging.PagingSource
import androidx.paging.PagingState
import net.pengcook.android.data.model.SearchData

class SearchPagingSource : PagingSource<Int, SearchData>() {
override fun getRefreshKey(state: PagingState<Int, SearchData>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SearchData> {
return try {
val page = params.key ?: 0
// Start paging with the STARTING_KEY if this is the first load
val start = params.key ?: STARTING_KEY
// Load as many items as hinted by params.loadSize
val range = start.until(start + params.loadSize)

val data =
range.map { number ->
SearchData(
// Generate consecutive increasing numbers as the article id
id = number.toLong(),
imageUrl = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTa6wAyeNbJl6jU-OAz4pTCeczAuPaXSWLTcw&s",
)
}

val prevKey = if (page == 0) null else page - 1
val nextKey = if (data.isEmpty()) null else page + 1

println("nextKey : $nextKey")

LoadResult.Page(
data = data,
prevKey = prevKey,
nextKey = nextKey,
)
} catch (exception: Exception) {
return LoadResult.Error(exception)
}
}

companion object {
private const val STARTING_KEY = 0
private const val LOAD_DELAY_MILLIS = 3_000L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.pengcook.android.data.model

data class SearchData(
val id: Long,
val imageUrl: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package net.pengcook.android.presentation.search

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import net.pengcook.android.data.model.SearchData
import net.pengcook.android.databinding.ItemSearchImageBinding

class SearchAdapter : PagingDataAdapter<SearchData, SearchAdapter.SearchViewHolder>(diffUtil) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): SearchViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemSearchImageBinding.inflate(layoutInflater)
return SearchViewHolder(binding)
}

override fun onBindViewHolder(
holder: SearchViewHolder,
position: Int,
) {
val item = getItem(position)
if (item != null) holder.bind(item)
}

class SearchViewHolder(private val binding: ItemSearchImageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: SearchData) {
binding.imageUrl = item.imageUrl
}
}

companion object {
val diffUtil =
object : DiffUtil.ItemCallback<SearchData>() {
override fun areItemsTheSame(
oldItem: SearchData,
newItem: SearchData,
): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(
oldItem: SearchData,
newItem: SearchData,
): Boolean {
return oldItem == newItem
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.pengcook.android.presentation.search

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pengcook.android.databinding.FragmentSearchBinding

class SearchFragment : Fragment() {
private val adapter: SearchAdapter by lazy { SearchAdapter() }
private var _binding: FragmentSearchBinding? = null
private val binding: FragmentSearchBinding
get() = _binding!!
private val viewModel: SearchViewModel by viewModels()

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

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

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun observeViewModel() {
viewModel.items.observe(viewLifecycleOwner) { pagingData ->
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.Main) {
adapter.submitData(pagingData)
}
}
}
}

private fun setUpBindingVariables() {
binding.lifecycleOwner = this
binding.adapter = adapter
binding.viewModel = viewModel
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.pengcook.android.presentation.search

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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 androidx.paging.liveData
import net.pengcook.android.data.datasource.SearchPagingSource
import net.pengcook.android.data.model.SearchData

class SearchViewModel : ViewModel() {
val searchKeyword: MutableLiveData<String> = MutableLiveData()

val items: LiveData<PagingData<SearchData>> =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
pagingSourceFactory = { SearchPagingSource() },
)
.liveData
.cachedIn(viewModelScope)

companion object {
private const val PAGE_SIZE = 10
}
}

42 changes: 42 additions & 0 deletions android/app/src/main/res/layout/fragment_search.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

<variable
name="viewModel"
type="net.pengcook.android.presentation.search.SearchViewModel" />

<variable
name="adapter"
type="net.pengcook.android.presentation.search.SearchAdapter" />
</data>

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<include
layout="@layout/item_appbar_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@{@string/search_appbar}"
app:searchKeyword="@={viewModel.searchKeyword}"/>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="12dp"
android:adapter="@{adapter}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>



</layout>
8 changes: 7 additions & 1 deletion android/app/src/main/res/navigation/nav_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
android:id="@+id/categoryFragment"
android:name="net.pengcook.android.presentation.category.CategoryFragment"
android:label="fragment_category"
tools:layout="@layout/item_category" />
tools:layout="@layout/fragment_category" />

<fragment
android:id="@+id/searchFragment"
android:name="net.pengcook.android.presentation.search.SearchFragment"
android:label="fragment_search"
tools:layout="@layout/fragment_search" />

</navigation>

0 comments on commit 8796094

Please sign in to comment.