diff --git a/android/app/src/main/java/net/pengcook/android/data/datasource/SearchPagingSource.kt b/android/app/src/main/java/net/pengcook/android/data/datasource/SearchPagingSource.kt new file mode 100644 index 00000000..b835a5af --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/datasource/SearchPagingSource.kt @@ -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() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + 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 + } +} diff --git a/android/app/src/main/java/net/pengcook/android/data/model/SearchData.kt b/android/app/src/main/java/net/pengcook/android/data/model/SearchData.kt new file mode 100644 index 00000000..ee7a5ee3 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/model/SearchData.kt @@ -0,0 +1,6 @@ +package net.pengcook.android.data.model + +data class SearchData( + val id: Long, + val imageUrl: String, +) diff --git a/android/app/src/main/java/net/pengcook/android/presentation/search/SearchAdapter.kt b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchAdapter.kt new file mode 100644 index 00000000..3ac2ecfc --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchAdapter.kt @@ -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(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() { + override fun areItemsTheSame( + oldItem: SearchData, + newItem: SearchData, + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: SearchData, + newItem: SearchData, + ): Boolean { + return oldItem == newItem + } + } + } +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/search/SearchFragment.kt b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchFragment.kt new file mode 100644 index 00000000..94296f55 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchFragment.kt @@ -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 + } +} + diff --git a/android/app/src/main/java/net/pengcook/android/presentation/search/SearchViewModel.kt b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchViewModel.kt new file mode 100644 index 00000000..f093fac4 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/search/SearchViewModel.kt @@ -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 = MutableLiveData() + + val items: LiveData> = + Pager( + config = PagingConfig(pageSize = PAGE_SIZE), + pagingSourceFactory = { SearchPagingSource() }, + ) + .liveData + .cachedIn(viewModelScope) + + companion object { + private const val PAGE_SIZE = 10 + } +} + diff --git a/android/app/src/main/res/layout/fragment_search.xml b/android/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 00000000..e8063e20 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/navigation/nav_graph.xml b/android/app/src/main/res/navigation/nav_graph.xml index 6a20c3c4..a19a918b 100644 --- a/android/app/src/main/res/navigation/nav_graph.xml +++ b/android/app/src/main/res/navigation/nav_graph.xml @@ -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" /> + +