I'm currently following a news app tutorial and I have a problem. When I type in a keyword in the edit text widget, articles related to that keyword shows up in the recycler view but when I erase that keyword to type in another keyword, the articles (in the recycler view) from the previous search query doesn't update and even when I exit the search fragment and open it again,The recycler view remains stagnant instead of disappearing. Can anyone please take a look at my code and let me know what I've done wrong. Thanks in advance.
Here is my code:
Search Fragment
`class SearchNewsFragment : Fragment(R.layout.fragment_search_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
val TAG = "SearchNewsFragment"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as NewsActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
findNavController().navigate(
R.id.action_searchNewsFragment_to_articleFragment,
bundle
)
}
var job: Job? = null
etSearch.addTextChangedListener { editable ->
job?.cancel()
job = MainScope().launch {
delay(SEARCH_NEWS_TIME_DELAY)
editable?.let {
if(editable.toString().isNotEmpty()) {
viewModel.searchNews(editable.toString())
}
}
}
}
viewModel.searchNews.observe(viewLifecycleOwner, Observer { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
hideErrorMessage()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / Constants.QUERY_PAGE_SIZE + 2
isLastPage = viewModel.searchNewsPage == totalPages
if(isLastPage) {
rvSearchNews.setPadding(0, 0, 0, 0)
}
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Toast.makeText(activity, "An error occured: $message", Toast.LENGTH_LONG).show()
showErrorMessage(message)
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
btnRetry.setOnClickListener {
if (etSearch.text.toString().isNotEmpty()) {
viewModel.searchNews(etSearch.text.toString())
} else {
hideErrorMessage()
}
}
}
private fun hideProgressBar() {
paginationProgressBar.visibility = View.INVISIBLE
isLoading = false
}
private fun showProgressBar() {
paginationProgressBar.visibility = View.VISIBLE
isLoading = true
}
private fun hideErrorMessage() {
itemErrorMessage.visibility = View.INVISIBLE
isError = false
}
private fun showErrorMessage(message: String) {
itemErrorMessage.visibility = View.VISIBLE
tvErrorMessage.text = message
isError = true
}
var isError = false
var isLoading = false
var isLastPage = false
var isScrolling = false
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val isNoErrors = !isError
val isNotLoadingAndNotLastPage = !isLoading && !isLastPage
val isAtLastItem = firstVisibleItemPosition + visibleItemCount >= totalItemCount
val isNotAtBeginning = firstVisibleItemPosition >= 0
val isTotalMoreThanVisible = totalItemCount >= Constants.QUERY_PAGE_SIZE
val shouldPaginate = isNoErrors && isNotLoadingAndNotLastPage && isAtLastItem && isNotAtBeginning &&
isTotalMoreThanVisible && isScrolling
if(shouldPaginate) {
viewModel.searchNews(etSearch.text.toString())
isScrolling = false
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
}
private fun setupRecyclerView() {
newsAdapter = NewsAdapter()
rvSearchNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
addOnScrollListener(this#SearchNewsFragment.scrollListener)
}
}
}`
SearchNewsAdapter
`class SearchNewsAdapter : RecyclerView.Adapter<SearchNewsAdapter.ArticleViewHolder>() {
// Inner class for viewHolder
inner class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private val differCallback = object : DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url== newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
//recyclerViewFunction
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.search_article_preview,parent, false)
)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply{
// Glide.with(this).load(article.urlToImage).into(ivArticleImage)
searchTitle.text = article.title
setOnClickListener{
onItemClickListener?.let{
it(article)
}
}
}
}
//item click listener to single article so that article fragment opens up the webview that shows our items
private var onItemClickListener: ((Article) -> Unit)? = null
fun setOnItemClickListener(listener:(Article) -> Unit){
onItemClickListener = listener
}
}`
NewsViewModel
`class NewsViewModel(
app: Application,
val newsRepository: NewsRepository
) : AndroidViewModel(app) {
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var breakingNewsPage = 1
var breakingNewsResponse: NewsResponse? = null
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchNewsPage = 1
var searchNewsResponse: NewsResponse? = null
var newSearchQuery:String? = null
var oldSearchQuery:String? = null
init {
getBreakingNews("us")
}
fun getBreakingNews(countryCode: String) = viewModelScope.launch {
safeBreakingNewsCall(countryCode)
}
fun searchNews(searchQuery: String) = viewModelScope.launch {
safeSearchNewsCall(searchQuery)
}
private fun handleBreakingNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse> {
if(response.isSuccessful) {
response.body()?.let { resultResponse ->
breakingNewsPage++
if(breakingNewsResponse == null) {
breakingNewsResponse = resultResponse
} else {
val oldArticles = breakingNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(breakingNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
private fun handleSearchNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse> {
if(response.isSuccessful) {
response.body()?.let { resultResponse ->
if(searchNewsResponse == null || newSearchQuery != oldSearchQuery) {
searchNewsPage = 1
oldSearchQuery = newSearchQuery
searchNewsResponse = resultResponse
} else {
searchNewsPage++
val oldArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
fun saveArticle(article: Article) = viewModelScope.launch {
newsRepository.upsert(article)
}
fun getSavedNews() = newsRepository.getSavedNews()
fun deleteArticle(article: Article) = viewModelScope.launch {
newsRepository.deleteArticle(article)
}
private suspend fun safeSearchNewsCall(searchQuery: String) {
newSearchQuery = searchQuery
searchNews.postValue(Resource.Loading())
try {
if(hasInternetConnection()) {
val response = newsRepository.searchNews(searchQuery, searchNewsPage)
searchNews.postValue(handleSearchNewsResponse(response))
} else {
searchNews.postValue(Resource.Error("No internet connection"))
}
} catch(t: Throwable) {
when(t) {
is IOException -> searchNews.postValue(Resource.Error("Network Failure"))
else -> searchNews.postValue(Resource.Error("Conversion Error"))
}
}
}
private suspend fun safeBreakingNewsCall(countryCode: String) {
breakingNews.postValue(Resource.Loading())
try {
if(hasInternetConnection()) {
val response = newsRepository.getBreakingNews(countryCode, breakingNewsPage)
breakingNews.postValue(handleBreakingNewsResponse(response))
} else {
breakingNews.postValue(Resource.Error("No internet connection"))
}
} catch(t: Throwable) {
when(t) {
is IOException -> breakingNews.postValue(Resource.Error("Network Failure"))
else -> breakingNews.postValue(Resource.Error("Conversion Error"))
}
}
}
private fun hasInternetConnection(): Boolean {
val connectivityManager = getApplication<NewsApplication>().getSystemService(
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
return when {
capabilities.hasTransport(TRANSPORT_WIFI) -> true
capabilities.hasTransport(TRANSPORT_CELLULAR) -> true
capabilities.hasTransport(TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
connectivityManager.activeNetworkInfo?.run {
return when(type) {
TYPE_WIFI -> true
TYPE_MOBILE -> true
TYPE_ETHERNET -> true
else -> false
}
}
}
return false
}
}
`
if(editable.toString().isNotEmpty()) { viewModel.searchNews(editable.toString()) }
this line of code prevent the empty query to be processed. So, when you delete everything from the edit text, it will do nothing, hence the result still the same.
even when I exit the search fragment and open it again,The recycler view remains stagnant instead of disappearing.
The search result is strored on NewsViewModel and because the ViewModel is initialized on the NewsActivity, it tied to the activity lifecycle. Even if you destroy the fragment, the search result (the whole ViewModel) will be kept because the activity is still alive. So, when you open back the search Fragment, the LiveData will give you the latest value.
Related
I have 4 different "Card List", "Card Magazine" , "Title" and "Grid" and view holders for each one to relate check my other question here.
now I am trying to change the layout automatically when the device rotates so when orientation is a portrait the layout be LinearLayout "Card layout" and when orientation changes to landscape the layout will be GridLayout, also I have a changeAndSaveLayout method to make the user choose between each layout from option menu
and I save the layout in ViewModel using DataStore and Flow,
The problem
When I rotate the device the RecyclerView and the list is gone and I see the empty screen,
and when I back to portrait the list is back it's back to default layout "cardLayout"
I tried multiple methods like notifyDataSetChanged after changing layout and handle changes in onConfigurationChanged methods but all these methods fails
DataStore class code saveRecyclerViewLayout and readRecyclerViewLayout
private val Context.dataStore by preferencesDataStore("user_preferences")
private const val TAG = "DataStoreRepository"
#ActivityRetainedScoped
class DataStoreRepository #Inject constructor(#ApplicationContext private val context: Context) {
suspend fun saveRecyclerViewLayout(
recyclerViewLayout: String,
) {
datastore.edit { preferences ->
preferences[PreferencesKeys.RECYCLER_VIEW_LAYOUT_KEY] = recyclerViewLayout
}
}
val readRecyclerViewLayout:
Flow<String> = datastore.data.catch { ex ->
if (ex is IOException) {
ex.message?.let { Log.e(TAG, it) }
emit(emptyPreferences())
} else {
throw ex
}
}.map { preferences ->
val recyclerViewLayout: String =
preferences[PreferencesKeys.RECYCLER_VIEW_LAYOUT_KEY] ?: "cardLayout"
recyclerViewLayout
}
}
I used it in ViewModel like the following
#HiltViewModel
class PostViewModel #Inject constructor(
private val mainRepository: MainRepository,
private val dataStoreRepository: DataStoreRepository,
application: Application
) :
AndroidViewModel(application) {
val recyclerViewLayout = dataStoreRepository.readRecyclerViewLayout.asLiveData()
fun saveRecyclerViewLayout(layout: String) {
viewModelScope.launch {
dataStoreRepository.saveRecyclerViewLayout(layout)
}
}
}
PostAdapter Class
class PostAdapter(
private val titleAndGridLayout: TitleAndGridLayout,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var viewType = 0
private val differCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return (oldItem == newItem)
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (this.viewType) {
CARD -> {
fromCardViewHolder(parent)
}
CARD_MAGAZINE -> {
fromCardMagazineViewHolder(parent)
}
TITLE -> {
fromTitleViewHolder(parent)
}
else -> {
fromGridViewHolder(parent)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item: Item = differ.currentList[position]
when (this.viewType) {
CARD -> if (holder is CardViewHolder) {
holder.bind(item)
}
CARD_MAGAZINE -> if (holder is CardMagazineViewHolder) {
holder.bind(item)
}
TITLE -> if (holder is TitleViewHolder) {
holder.bind(item)
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems()
}
GRID -> if (holder is GridViewHolder) {
holder.bind(item)
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems()
}
}
}
override fun getItemCount(): Int {
return differ.currentList.size
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
class CardViewHolder(private val cardLayoutBinding: CardLayoutBinding) :
RecyclerView.ViewHolder(cardLayoutBinding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
var date: Date? = Date()
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
cardLayoutBinding.postTitle.text = item.title
try {
Glide.with(cardLayoutBinding.root).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(cardLayoutBinding.postImage)
} catch (e: IndexOutOfBoundsException) {
cardLayoutBinding.postImage.setImageResource(R.drawable.no_image)
}
cardLayoutBinding.postDescription.text = document.text()
try {
date = format.parse(item.published)
} catch (e: ParseException) {
e.printStackTrace()
}
val prettyTime = PrettyTime()
cardLayoutBinding.postDate.text = prettyTime.format(date)
}
}
class CardMagazineViewHolder(private val cardMagazineBinding: CardMagazineBinding) :
RecyclerView.ViewHolder(cardMagazineBinding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
var date: Date? = Date()
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
cardMagazineBinding.postTitle.text = item.title
try {
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(cardMagazineBinding.postImage)
} catch (e: IndexOutOfBoundsException) {
cardMagazineBinding.postImage.setImageResource(R.drawable.no_image)
}
try {
date = format.parse(item.published)
} catch (e: ParseException) {
e.printStackTrace()
}
val prettyTime = PrettyTime()
cardMagazineBinding.postDate.text = prettyTime.format(date)
}
}
class TitleViewHolder(private val binding: TitleLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
binding.postTitle.text = item.title
try {
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(binding.postImage)
} catch (e: IndexOutOfBoundsException) {
binding.postImage.setImageResource(R.drawable.no_image)
}
}
}
class GridViewHolder constructor(private val binding: GridLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
binding.postTitle.text = item.title
try {
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(binding.postImage)
} catch (e: IndexOutOfBoundsException) {
binding.postImage.setImageResource(R.drawable.no_image)
}
}
}
companion object {
private const val CARD = 0
private const val CARD_MAGAZINE = 1
private const val TITLE = 2
private const val GRID = 3
private const val TAG = "POST_ADAPTER"
fun fromCardViewHolder(parent: ViewGroup): CardViewHolder {
val cardLayoutBinding: CardLayoutBinding =
CardLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CardViewHolder(cardLayoutBinding)
}
fun fromCardMagazineViewHolder(parent: ViewGroup): CardMagazineViewHolder {
val cardMagazineBinding: CardMagazineBinding =
CardMagazineBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CardMagazineViewHolder(cardMagazineBinding)
}
fun fromTitleViewHolder(parent: ViewGroup): TitleViewHolder {
val titleLayoutBinding: TitleLayoutBinding =
TitleLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TitleViewHolder(titleLayoutBinding)
}
fun fromGridViewHolder(
parent: ViewGroup
): GridViewHolder {
val gridLayoutBinding: GridLayoutBinding =
GridLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return GridViewHolder(gridLayoutBinding)
}
}
init {
setHasStableIds(true)
}
}
and finally the HomeFragment
#AndroidEntryPoint
class HomeFragment : Fragment(), TitleAndGridLayout, MenuProvider {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private var itemArrayList = arrayListOf<Item>()
private var searchItemList = arrayListOf<Item>()
private val postViewModel: PostViewModel by viewModels()
private var linearLayoutManager: LinearLayoutManager? = null
private val titleLayoutManager: GridLayoutManager by lazy {
GridLayoutManager(requireContext(), 2)
}
private val gridLayoutManager: GridLayoutManager by lazy {
GridLayoutManager(requireContext(), 3)
}
private var menuHost: MenuHost? = null
private lateinit var networkListener: NetworkListener
private lateinit var adapter:PostAdapter
private var isScrolling = false
var currentItems = 0
var totalItems: Int = 0
var scrollOutItems: Int = 0
private var postsAPiFlag = false
private val recyclerStateKey = "recycler_state"
private val mBundleRecyclerViewState by lazy { Bundle() }
private var keyword: String? = null
private var orientation: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postViewModel.finalURL.value = "$BASE_URL?key=$API_KEY"
networkListener = NetworkListener()
}
// This property is only valid between onCreateView and
// onDestroyView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
adapter = PostAdapter(this)
orientation = resources.configuration.orientation
_binding = FragmentHomeBinding.inflate(inflater, container, false)
menuHost = requireActivity()
menuHost?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.CREATED)
postViewModel.recyclerViewLayout.observe(viewLifecycleOwner) { layout ->
linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
Log.w(TAG, "getSavedLayout called")
Log.w(TAG, "getSavedLayout: orientation ${orientation.toString()}", )
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
when (layout) {
"cardLayout" -> {
//
adapter.viewType = 0
binding.apply {
homeRecyclerView.layoutManager = linearLayoutManager
homeRecyclerView.adapter = adapter
}
}
"cardMagazineLayout" -> {
// binding.loadMoreBtn.visibility = View.VISIBLE
binding.homeRecyclerView.layoutManager = linearLayoutManager
adapter.viewType = 1
binding.homeRecyclerView.adapter = adapter
}
}
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
when (layout) {
"titleLayout" -> {
// binding.loadMoreBtn.visibility = View.GONE
binding.homeRecyclerView.layoutManager = titleLayoutManager
adapter.viewType = 2
binding.homeRecyclerView.adapter = adapter
}
"gridLayout" -> {
binding.homeRecyclerView.layoutManager = gridLayoutManager
adapter.viewType = 3
binding.homeRecyclerView.adapter = adapter
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.change_layout) {
changeAndSaveLayout()
true
} else false
}
private fun changeAndSaveLayout() {
// Log.w(TAG, "changeAndSaveLayout: called")
val builder = AlertDialog.Builder(requireContext())
builder.setTitle(getString(R.string.choose_layout))
val recyclerViewPortraitLayout =
resources.getStringArray(R.array.RecyclerViewPortraitLayout)
val recyclerViewLandscapeLayout =
resources.getStringArray(R.array.RecyclerViewLandscapeLayout)
// SharedPreferences.Editor editor = sharedPreferences.edit();
Log.d(TAG, "changeAndSaveLayout: ${orientation.toString()}")
if (orientation == 1) {
builder.setItems(
recyclerViewPortraitLayout
) { _: DialogInterface?, index: Int ->
try {
when (index) {
0 -> {
adapter.viewType = 0
binding.homeRecyclerView.layoutManager = linearLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("cardLayout")
}
1 -> {
adapter.viewType = 1
binding.homeRecyclerView.layoutManager = linearLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("cardMagazineLayout")
}
}
} catch (e: Exception) {
Log.e(TAG, "changeAndSaveLayout: " + e.message)
Log.e(TAG, "changeAndSaveLayout: " + e.cause)
}
}
} else if (orientation == 2) {
builder.setItems(
recyclerViewLandscapeLayout
) { _: DialogInterface?, index: Int ->
try {
when (index) {
2 -> {
adapter.viewType = 2
binding.homeRecyclerView.layoutManager = titleLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("titleLayout")
}
3 -> {
adapter.viewType = 3
binding.homeRecyclerView.layoutManager = gridLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("gridLayout")
}
}
} catch (e: Exception) {
Log.e(TAG, "changeAndSaveLayout: " + e.message)
Log.e(TAG, "changeAndSaveLayout: " + e.cause)
}
}
}
val alertDialog = builder.create()
alertDialog.show()
}
}
GIF showing the problem
since long time I was looking for a soultion and i found it and added it to my old project, i was use shared prefernces but in your case i mean data store it will work normally
you need to create two arrays for each orintation
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="recyclerViewPortraitList">
<item>Card List</item>
<item>Card Magazine</item>
<item>Title</item>
</array>
<array name="recyclerViewLandscapeList">
<item>Grid with 3 Span</item>
<item>Grid with 4 Span</item>
</array>
</resources>
in your data store will be like the following
private object PreferencesKeys {
var RECYCLER_VIEW_PORTRAIT_LAYOUT_KEY = stringPreferencesKey("recyclerViewPortraitLayout")
var RECYCLER_VIEW_LANDSCAPE_LAYOUT_KEY = stringPreferencesKey("recyclerViewLandscapeLayout")
}
suspend fun saveRecyclerViewPortraitLayout(
recyclerViewLayout: String,
) {
datastore.edit { preferences ->
preferences[PreferencesKeys.RECYCLER_VIEW_PORTRAIT_LAYOUT_KEY] = recyclerViewLayout
}
}
suspend fun saveRecyclerViewLandscapeLayout(recyclerViewLayout: String) {
datastore.edit { preferences ->
preferences[PreferencesKeys.RECYCLER_VIEW_LANDSCAPE_LAYOUT_KEY] = recyclerViewLayout
}
}
val readRecyclerViewPortraitLayout:
Flow<String> = datastore.data.catch { ex ->
if (ex is IOException) {
ex.message?.let { Log.e(TAG, it) }
emit(emptyPreferences())
} else {
throw ex
}
}.map { preferences ->
val recyclerViewLayout: String =
preferences[PreferencesKeys.RECYCLER_VIEW_PORTRAIT_LAYOUT_KEY] ?: "cardLayout"
recyclerViewLayout
}
val readRecyclerViewLandscpaeLayout:
Flow<String> = datastore.data.catch { ex ->
if (ex is IOException) {
ex.message?.let { Log.e(TAG, it) }
emit(emptyPreferences())
} else {
throw ex
}
}.map { preferences ->
val recyclerViewLayout: String =
preferences[PreferencesKeys.RECYCLER_VIEW_LANDSCAPE_LAYOUT_KEY] ?: "gridWith3Span"
recyclerViewLayout
}
in the viewModel
val readRecyclerViewPortraitLayout =
dataStoreRepository.readRecyclerViewPortraitLayout.asLiveData()
val readRecyclerViewLandscapeLayout =
dataStoreRepository.readRecyclerViewLandscpaeLayout.asLiveData()
fun saveRecyclerViewPortraitLayout(layout: String) {
viewModelScope.launch {
dataStoreRepository.saveRecyclerViewPortraitLayout(layout)
}
}
fun saveRecyclerViewLandscapeLayout(layout: String) {
viewModelScope.launch {
dataStoreRepository.saveRecyclerViewLandscapeLayout(layout)
}
}
in adapter change constants
companion object {
private const val CARD = 0
private const val CARD_MAGAZINE = 1
private const val TITLE = 2
private const val GRID_WITH_3_SPAN = 3
private const val GRID_WITH_4_SPAN = 4
}
5.and finally in the fragment or activity you can use it like the following
private fun setUpRecyclerViewLayout() {
if (requireActivity().resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
postViewModel.readRecyclerViewPortraitLayout.observe(viewLifecycleOwner) { layout ->
recyclerViewLayout = layout
when (layout) {
"cardLayout" -> {
adapter.viewType = 0
binding.apply {
homeRecyclerView.layoutManager = linearLayoutManager
homeRecyclerView.adapter = adapter
homeRecyclerView.setHasFixedSize(true)
}
}
"cardMagazineLayout" -> {
binding.homeRecyclerView.layoutManager = linearLayoutManager
adapter.viewType = 1
binding.homeRecyclerView.adapter = adapter
}
"titleLayout" -> {
binding.homeRecyclerView.layoutManager = titleLayoutManager
adapter.viewType = 2
binding.homeRecyclerView.adapter = adapter
}
}
}
} else {
postViewModel.readRecyclerViewLandscapeLayout.observe(viewLifecycleOwner) { layout ->
recyclerViewLayout = layout
when (layout) {
"gridWith3Span" -> {
binding.homeRecyclerView.layoutManager = gridWith3SpanLayoutManager
adapter.viewType = 3
binding.homeRecyclerView.adapter = adapter
}
"gridWith4Span" -> {
binding.homeRecyclerView.layoutManager = gridWith4SpanLayoutManager
adapter.viewType = 4
binding.homeRecyclerView.adapter = adapter
}
}
}
}
}
private fun changeAndSaveLayout() {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle(getString(R.string.choose_layout))
// SharedPreferences.Editor editor = sharedPreferences.edit();
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
builder.setItems(
resources.getStringArray(R.array.recyclerViewPortraitList)
) { _: DialogInterface?, index: Int ->
try {
when (index) {
0 -> {
adapter.viewType = 0
binding.homeRecyclerView.layoutManager = linearLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewPortraitLayout("cardLayout")
}
1 -> {
adapter.viewType = 1
binding.homeRecyclerView.layoutManager = linearLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewPortraitLayout("cardMagazineLayout")
}
2 -> {
adapter.viewType = 2
binding.homeRecyclerView.layoutManager = titleLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewPortraitLayout("titleLayout")
}
else -> {
throw Exception("Unknown layout")
}
}
} catch (e: Exception) {
Log.e(TAG, "changeAndSaveLayout: " + e.message)
Log.e(TAG, "changeAndSaveLayout: " + e.cause)
}
}
} else {
builder.setItems(
resources.getStringArray(R.array.recyclerViewLandscapeList)
) { _: DialogInterface?, index: Int ->
try {
when (index) {
0 -> {
adapter.viewType = 3
binding.homeRecyclerView.layoutManager = gridWith3SpanLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLandscapeLayout("gridWith3Span")
}
1 -> {
adapter.viewType = 4
binding.homeRecyclerView.layoutManager = gridWith4SpanLayoutManager
binding.homeRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLandscapeLayout("gridWith4Span")
}
else -> {
throw Exception("Unknown layout")
}
}
} catch (e: Exception) {
Log.e(TAG, "changeAndSaveLayout: " + e.message)
Log.e(TAG, "changeAndSaveLayout: " + e.cause)
}
}
}
val alertDialog = builder.create()
alertDialog.show()
}
if you setting configuration changes in manifest like that android:configChanges="orientation|screenSize"you will need to call the setUpRecyclerViewLayout() from onConfigurationChanged
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setUpRecyclerViewLayout()
}
I just noticed problem earlier in my app, I see the ViewModel inside fragment doesn't save/keep recycler view when I rotate the device, I don't want to use the old method like save data in bundle onSaveInstanceState and restore it, I tried to figure why this problem by printing some logs on each method in fragment lifecycle but I didn't succeed
GIF showing the problem
the ViewModel
#HiltViewModel
class PostViewModel #Inject constructor(
private val mainRepository: MainRepository,
private val dataStoreRepository: DataStoreRepository,
application: Application
) :
AndroidViewModel(application) {
/** ROOM DATABASE */
val readAllPosts: LiveData<List<Item>> = mainRepository.localDataSource.getAllItems().asLiveData()
val postsBySearchInDB: MutableLiveData<List<Item>> = MutableLiveData()
/** RETROFIT **/
var postsResponse: MutableLiveData<NetworkResult<PostList>> = MutableLiveData()
var searchedPostsResponse: MutableLiveData<NetworkResult<PostList>> = MutableLiveData()
var postListResponse: PostList? = null
var postListByLabelResponse: PostList? = null
var searchPostListResponse: PostList? = null
val label = MutableLiveData<String>()
var finalURL: MutableLiveData<String?> = MutableLiveData()
val token = MutableLiveData<String?>()
val currentDestination = MutableLiveData<Int>()
fun getCurrentDestination() {
viewModelScope.launch {
dataStoreRepository.readCurrentDestination.collect {
currentDestination.value = it
}
}
}
val errorCode = MutableLiveData<Int>()
val searchError = MutableLiveData<Boolean>()
var networkStats = false
var backOnline = false
val recyclerViewLayout = dataStoreRepository.readRecyclerViewLayout.asLiveData()
val readBackOnline = dataStoreRepository.readBackOnline.asLiveData()
override fun onCleared() {
super.onCleared()
finalURL.value = null
token.value = null
}
private fun saveBackOnline(backOnline: Boolean) = viewModelScope.launch {
dataStoreRepository.saveBackOnline(backOnline)
}
fun saveCurrentDestination(currentDestination: Int) {
viewModelScope.launch {
dataStoreRepository.saveCurrentDestination(currentDestination)
}
}
fun saveRecyclerViewLayout(layout: String) {
viewModelScope.launch {
dataStoreRepository.saveRecyclerViewLayout(layout)
}
}
fun getPosts() = viewModelScope.launch {
getPostsSafeCall()
}
fun getPostListByLabel() = viewModelScope.launch {
getPostsByLabelSafeCall()
}
fun getItemsBySearch() = viewModelScope.launch {
getItemsBySearchSafeCall()
}
private suspend fun getPostsByLabelSafeCall() {
postsResponse.value = NetworkResult.Loading()
if (hasInternetConnection()) {
try {
val response = mainRepository.remoteDataSource.getPostListByLabel(finalURL.value!!)
postsResponse.value = handlePostsByLabelResponse(response)
} catch (ex: HttpException) {
Log.e(TAG, ex.message + ex.cause)
postsResponse.value = NetworkResult.Error(ex.message.toString())
errorCode.value = ex.code()
} catch (ex: NullPointerException) {
postsResponse.value = NetworkResult.Error("There's no items")
}
} else {
postsResponse.value = NetworkResult.Error("No Internet Connection.")
}
}
private suspend fun getPostsSafeCall() {
postsResponse.value = NetworkResult.Loading()
if (hasInternetConnection()) {
try {
if (finalURL.value.isNullOrEmpty()) {
finalURL.value = "$BASE_URL?key=$API_KEY"
}
val response = mainRepository.remoteDataSource.getPostList(finalURL.value!!)
postsResponse.value = handlePostsResponse(response)
} catch (e: Exception) {
postsResponse.value = NetworkResult.Error(e.message.toString())
if (e is HttpException) {
errorCode.value = e.code()
Log.e(TAG, "getPostsSafeCall: errorCode $errorCode")
Log.e(TAG, "getPostsSafeCall: ${e.message.toString()}")
}
}
} else {
postsResponse.value = NetworkResult.Error("No Internet Connection.")
}
}
private fun handlePostsResponse(response: Response<PostList>): NetworkResult<PostList> {
if (response.isSuccessful) {
if (!(token.value.equals(response.body()?.nextPageToken))) {
token.value = response.body()?.nextPageToken
response.body()?.let { resultResponse ->
Log.d(
TAG, "handlePostsResponse: old token is: ${token.value} " +
"new token is: ${resultResponse.nextPageToken}"
)
finalURL.value = "${BASE_URL}?pageToken=${token.value}&key=${API_KEY}"
Log.e(TAG, "handlePostsResponse finalURL is ${finalURL.value!!}")
for (item in resultResponse.items) {
insertItem(item)
}
return NetworkResult.Success(resultResponse)
}
}
}
if (token.value == null) {
errorCode.value = 400
} else {
errorCode.value = response.code()
}
return NetworkResult.Error(
"network results of handlePostsResponse ${
response.body().toString()
}"
)
}
private fun handlePostsByLabelResponse(response: Response<PostList>): NetworkResult<PostList> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
Log.d(
TAG, "handlePostsByLabelResponse: old token is: ${token.value} " +
"new token is: ${resultResponse.nextPageToken}"
)
finalURL.postValue(
(BASE_URL_POSTS_BY_LABEL + "posts?labels=${label.value}"
+ "&maxResults=20"
+ "&pageToken=")
+ token.value
+ "&key=" + API_KEY
)
if (postListByLabelResponse == null) {
postListByLabelResponse = resultResponse
} else {
val oldPosts = postListByLabelResponse?.items
val newPosts = resultResponse.items
oldPosts?.addAll(newPosts)
}
return NetworkResult.Success(postListByLabelResponse?:resultResponse)
}
}
if (token.value == null) {
errorCode.value = 400
} else {
errorCode.value = response.code()
}
Log.e(TAG, "handlePostsByLabelResponse: final URL ${finalURL.value}")
return NetworkResult.Error(
"network results of handlePostsByLabelResponse ${
response.body().toString()
}"
)
}
private fun hasInternetConnection(): Boolean {
val connectivityManager = getApplication<Application>().getSystemService(
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val capabilities =
connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
return when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> true
else -> false
}
} else {
val networkInfo = connectivityManager.activeNetworkInfo
return networkInfo != null && networkInfo.isConnectedOrConnecting
}
}
fun showNetworkStats() {
if (!networkStats) {
Toast.makeText(getApplication(), "No Internet connection", Toast.LENGTH_SHORT).show()
saveBackOnline(true)
} else if (networkStats) {
if (backOnline) {
Toast.makeText(getApplication(), "We're back online", Toast.LENGTH_SHORT).show()
saveBackOnline(false)
}
}
}
private fun insertItem(item: Item) {
viewModelScope.launch(Dispatchers.IO) {
mainRepository.localDataSource.insertItem(item)
}
}
private suspend fun getItemsBySearchSafeCall() {
searchedPostsResponse.value = NetworkResult.Loading()
if (!label.value.isNullOrEmpty()) {
finalURL.value = "${BASE_URL}?labels=${label.value}&maxResults=500&key=$API_KEY"
}
Log.e(TAG, "getItemsBySearch: ${finalURL.value}")
if (hasInternetConnection()) {
try {
val response = mainRepository.remoteDataSource.getPostListBySearch(finalURL.value!!)
searchedPostsResponse.value = handlePostsBySearchResponse(response)
} catch (e: Exception) {
searchedPostsResponse.value = NetworkResult.Error(e.message.toString())
}
} else {
searchedPostsResponse.value = NetworkResult.Error("No Internet Connection.")
}
}
private fun handlePostsBySearchResponse(response: Response<PostList>): NetworkResult<PostList> {
return if (response.isSuccessful) {
val postListResponse = response.body()
NetworkResult.Success(postListResponse!!)
} else {
errorCode.value = response.code()
NetworkResult.Error(response.errorBody().toString())
}
}
fun getItemsBySearchInDB(keyword: String) {
Log.d(TAG, "getItemsBySearchInDB: called")
viewModelScope.launch {
val items = mainRepository.localDataSource.getItemsBySearch(keyword)
if (items.isNotEmpty()) {
postsBySearchInDB.value = items
} else {
searchError.value = true
Log.e(TAG, "list is empty")
}
}
}
}
the fragment
#AndroidEntryPoint
class AccessoryFragment : Fragment(), MenuProvider, TitleAndGridLayout {
private var _binding: FragmentAccessoryBinding? = null
private val binding get() = _binding!!
private var searchItemList = arrayListOf<Item>()
private lateinit var postViewModel: PostViewModel
private val titleLayoutManager: GridLayoutManager by lazy { GridLayoutManager(context, 2) }
private val gridLayoutManager: GridLayoutManager by lazy { GridLayoutManager(context, 3) }
private var linearLayoutManager: LinearLayoutManager? = null
private val KEY_RECYCLER_STATE = "recycler_state"
private val mBundleRecyclerViewState by lazy { Bundle() }
private lateinit var adapter: PostAdapter
private var isScrolling = false
var currentItems = 0
var totalItems: Int = 0
var scrollOutItems: Int = 0
private var postsAPiFlag = false
private var keyword: String? = null
private lateinit var networkListener: NetworkListener
private var networkStats = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postViewModel = ViewModelProvider(this)[PostViewModel::class.java]
adapter = PostAdapter(this)
postViewModel.finalURL.value =
BASE_URL_POSTS_BY_LABEL + "posts?labels=Accessory&maxResults=20&key=$API_KEY"
networkListener = NetworkListener()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAccessoryBinding.inflate(inflater, container, false)
val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.CREATED)
postViewModel.label.value = "Accessory"
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "onViewCreated: called")
postViewModel.recyclerViewLayout.observe(viewLifecycleOwner) { layout ->
linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
Log.w(TAG, "onViewCreated getSavedLayout called")
when (layout) {
"cardLayout" -> {
binding.accessoryRecyclerView.layoutManager = linearLayoutManager
adapter.viewType = 0
binding.accessoryRecyclerView.adapter = adapter
}
"cardMagazineLayout" -> {
binding.accessoryRecyclerView.layoutManager = linearLayoutManager
adapter.viewType = 1
binding.accessoryRecyclerView.adapter = adapter
}
"titleLayout" -> {
binding.accessoryRecyclerView.layoutManager = titleLayoutManager
adapter.viewType = 2
binding.accessoryRecyclerView.adapter = adapter
}
"gridLayout" -> {
binding.accessoryRecyclerView.layoutManager = gridLayoutManager
adapter.viewType = 3
binding.accessoryRecyclerView.adapter = adapter
}
}
}
lifecycleScope.launchWhenStarted {
networkListener.checkNetworkAvailability(requireContext()).collect { stats ->
Log.d(TAG, "networkListener: $stats")
postViewModel.networkStats = stats
postViewModel.showNetworkStats()
this#AccessoryFragment.networkStats = stats
if (stats ) {
if (binding.accessoryRecyclerView.visibility == View.GONE) {
binding.accessoryRecyclerView.visibility = View.VISIBLE
}
requestApiData()
} else {
// Log.d(TAG, "onViewCreated: savedInstanceState $savedInstanceState")
noInternetConnectionLayout()
}
}
}
binding.accessoryRecyclerView.onItemClick { _, position, _ ->
val postItem = adapter.differ.currentList[position]
findNavController().navigate(
AccessoryFragmentDirections.actionNavAccessoryToDetailsActivity(
postItem
)
)
}
binding.accessoryRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
currentItems = linearLayoutManager!!.childCount
totalItems = adapter.itemCount
scrollOutItems = linearLayoutManager!!.findFirstVisibleItemPosition()
if ((!recyclerView.canScrollVertically(1) && dy > 0) &&
(isScrolling && currentItems + scrollOutItems >= totalItems && postsAPiFlag)
) {
hideShimmerEffect()
postViewModel.getPostListByLabel()
isScrolling = false
}
}
})
postViewModel.errorCode.observe(viewLifecycleOwner) { errorCode ->
if (errorCode == 400) {
binding.accessoryRecyclerView.setPadding(0, 0, 0, 0)
Toast.makeText(requireContext(), R.string.lastPost, Toast.LENGTH_LONG).show()
binding.progressBar.visibility = View.GONE
} else {
Log.e(TAG, "onViewCreated: ${postViewModel.errorCode.value.toString()} ")
noInternetConnectionLayout()
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Log.d(TAG, "onConfigurationChanged: ${newConfig.orientation}")
Log.d(TAG, "onConfigurationChanged: ${adapter.differ.currentList.toString()}")
Log.d(
TAG,
"onConfigurationChanged: " +
binding.accessoryRecyclerView.layoutManager?.itemCount.toString()
)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
Log.d(TAG, "onViewStateRestored: called")
}
private fun requestApiData() {
showShimmerEffect()
Log.d(TAG, "requestApiData: called")
postViewModel.getPostListByLabel()
postViewModel.postsResponse.observe(viewLifecycleOwner) { response ->
postsAPiFlag = true
when (response) {
is NetworkResult.Success -> {
hideShimmerEffect()
response.data?.let {
binding.progressBar.visibility = View.GONE
// itemArrayList.addAll(it.items)
adapter.differ.submitList(it.items.toList())
}
}
is NetworkResult.Error -> {
hideShimmerEffect()
binding.progressBar.visibility = View.GONE
Log.e(TAG, response.data.toString())
Log.e(TAG, response.message.toString())
}
is NetworkResult.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
}
}
}
private fun showShimmerEffect() {
binding.apply {
shimmerLayout.visibility = View.VISIBLE
accessoryRecyclerView.visibility = View.INVISIBLE
}
}
private fun hideShimmerEffect() {
binding.apply {
shimmerLayout.stopShimmer()
shimmerLayout.visibility = View.GONE
accessoryRecyclerView.visibility = View.VISIBLE
}
}
private fun changeAndSaveLayout() {
Log.w(TAG, "changeAndSaveLayout: called")
val builder = AlertDialog.Builder(requireContext())
builder.setTitle(getString(R.string.choose_layout))
val recyclerViewLayouts = resources.getStringArray(R.array.RecyclerViewLayouts)
// SharedPreferences.Editor editor = sharedPreferences.edit();
builder.setItems(
recyclerViewLayouts
) { _: DialogInterface?, index: Int ->
try {
when (index) {
0 -> {
adapter.viewType = 0
binding.accessoryRecyclerView.layoutManager = linearLayoutManager
binding.accessoryRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("cardLayout")
}
1 -> {
adapter.viewType = 1
binding.accessoryRecyclerView.layoutManager = linearLayoutManager
binding.accessoryRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("cardMagazineLayout")
}
2 -> {
adapter.viewType = 2
binding.accessoryRecyclerView.layoutManager = titleLayoutManager
binding.accessoryRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("titleLayout")
}
3 -> {
adapter.viewType = 3
binding.accessoryRecyclerView.layoutManager = gridLayoutManager
binding.accessoryRecyclerView.adapter = adapter
postViewModel.saveRecyclerViewLayout("gridLayout")
}
}
} catch (e: Exception) {
Log.e(TAG, "changeAndSaveLayout: " + e.message)
Log.e(TAG, "changeAndSaveLayout: " + e.cause)
}
}
val alertDialog = builder.create()
alertDialog.show()
}
private fun noInternetConnectionLayout() {
binding.apply {
// accessoryRecyclerView.removeAllViews()
Log.d(TAG, "noInternetConnectionLayout: called")
shimmerLayout.stopShimmer()
shimmerLayout.visibility = View.GONE
accessoryRecyclerView.visibility = View.GONE
}
binding.noInternetConnectionLayout.inflate()
binding.noInternetConnectionLayout.let {
if (networkStats) {
it.visibility = View.GONE
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// adapter.isDestroyed = true
linearLayoutManager?.removeAllViews()
// adView.destroy()
linearLayoutManager = null
_binding = null
}
override fun onDetach() {
super.onDetach()
if(linearLayoutManager != null){
linearLayoutManager = null
}
}
override fun tellFragmentToGetItems() {
if (postViewModel.recyclerViewLayout.value.equals("titleLayout")
|| postViewModel.recyclerViewLayout.value.equals("gridLayout")
) {
hideShimmerEffect()
postViewModel.getPostListByLabel()
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.main, menu)
val searchManager =
requireContext().getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = menu.findItem(R.id.app_bar_search).actionView as SearchView
searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
searchView.queryHint = resources.getString(R.string.searchForPosts)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(keyword: String): Boolean {
if (keyword.isEmpty()) {
Snackbar.make(
requireView(),
"please enter keyword to search",
Snackbar.LENGTH_SHORT
).show()
}
// itemArrayList.clear()
this#AccessoryFragment.keyword = keyword
requestSearchApi(keyword)
return false
}
override fun onQueryTextChange(newText: String): Boolean {
return false
}
})
searchView.setOnCloseListener {
if (keyword.isNullOrEmpty()) {
hideShimmerEffect()
return#setOnCloseListener false
}
if (Utils.hasInternetConnection(requireContext())) {
showShimmerEffect()
postViewModel.postListByLabelResponse = null
searchItemList.clear()
// adapter.differ.submitList(ArrayList())
linearLayoutManager?.removeAllViews()
binding.accessoryRecyclerView.removeAllViews()
// itemArrayList.clear()
adapter.differ.submitList(null)
postViewModel.finalURL.value =
BASE_URL_POSTS_BY_LABEL + "posts?labels=Accessory&maxResults=20&key=$API_KEY"
requestApiData()
// itemArrayList.clear()
// adapter.submitList(itemArrayList)
//====> Here I call the request api method again
Log.d(
TAG,
"setOnCloseListener: called ${adapter.differ.currentList.size.toString()}"
)
// adapter.notifyDataSetChanged()
// binding.progressBar.visibility = View.GONE
//
Log.d(TAG, "setOnCloseListener: ${postViewModel.finalURL.value.toString()}")
//
// adapter.notifyDataSetChanged()
// }
} else {
Log.d(TAG, "setOnCloseListener: called")
adapter.differ.submitList(null)
searchItemList.clear()
noInternetConnectionLayout()
}
false
}
postViewModel.searchError.observe(viewLifecycleOwner) { searchError ->
if (searchError) {
Toast.makeText(
requireContext(),
"There's no posts with this keyword", Toast.LENGTH_LONG
).show()
}
}
}
private fun requestSearchApi(keyword: String) {
if (Utils.hasInternetConnection(requireContext())) {
showShimmerEffect()
postViewModel.finalURL.value =
"${BASE_URL}?labels=Accessory&maxResults=500&key=$API_KEY"
postViewModel.getItemsBySearch()
postViewModel.searchedPostsResponse.observe(viewLifecycleOwner) { response ->
when (response) {
is NetworkResult.Success -> {
postsAPiFlag = false
// adapter.differ.currentList.clear()
if (searchItemList.isNotEmpty()) {
searchItemList.clear()
}
binding.progressBar.visibility = View.GONE
lifecycleScope.launch {
withContext(Dispatchers.Default) {
searchItemList.addAll(response.data?.items?.filter {
it.title.contains(keyword) || it.content.contains(keyword)
} as ArrayList<Item>)
}
}
Log.d(TAG, "requestSearchApi: test size ${searchItemList.size}")
if (searchItemList.isEmpty()) {
// adapter.differ.submitList(null)
Toast.makeText(
requireContext(),
"The search word was not found in any post",
Toast.LENGTH_SHORT
).show()
hideShimmerEffect()
return#observe
} else {
postsAPiFlag = false
// itemArrayList.clear()
adapter.differ.submitList(null)
hideShimmerEffect()
// Log.d(
//// TAG, "requestSearchApi: searchItemList ${searchItemList[0].title}"
// )
adapter.differ.submitList(searchItemList)
// binding.accessoryRecyclerView.scrollToPosition(0)
}
}
is NetworkResult.Error -> {
hideShimmerEffect()
binding.progressBar.visibility = View.GONE
Toast.makeText(
requireContext(),
response.message.toString(),
Toast.LENGTH_SHORT
).show()
Log.e(TAG, "onQueryTextSubmit: $response")
}
is NetworkResult.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
}
}
} else {
noInternetConnectionLayout()
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.change_layout) {
changeAndSaveLayout()
true
} else false
}
}
Unless I'm missing something (that's a lot of code to go through!) you don't set any data on your adapter until this bit:
private fun requestApiData() {
postViewModel.getPostListByLabel()
postViewModel.postsResponse.observe(viewLifecycleOwner) {
...
adapter.differ.submitList(it.items.toList())
}
And getPostListByLabel() clears the current data in postsResponse
fun getPostListByLabel() = viewModelScope.launch {
getPostsByLabelSafeCall()
}
private suspend fun getPostsByLabelSafeCall() {
postsResponse.value = NetworkResult.Loading()
// fetch data over network and update postsResponse with it later
...
}
So when you first observe it, it's in the NetworkResult.Loading state - any posts you had stored have been wiped.
Your Fragment gets recreated when the Activity is rotated and destroyed - so if you're initialising the ViewModel data contents as part of that Fragment setup (like you're doing here) it's going to get reinitialised every time the Fragment is recreated, and you'll lose the current data.
You'll need to work out a way to avoid that happening - you don't actually want to do that clear-and-fetch whenever a Fragment is created, so you'll have to decide when it should happen. Maybe when the ViewModel is first created (i.e. through the init block), maybe the first time a Fragment calls it (e.g. create an initialised boolean in the VM set to false, check it in the call, set true when it runs). Or maybe just when postsResponse has no value yet (postsResponse.value == null). I don't know the flow of your application so you'll have to work out when to force a fetch and when to keep the data that's already there
I am trying to display RESTful Api in recycler view in fragment. When app runs it shows no error but loads nothing in recycler view. I logged response in PagingSource file and it shows correct data but still nothing is displayed in recycler view.
This is my Api interface:
interface Api {
companion object {
const val BASE_URL = "http://be7c232bf30e.ngrok.io"
}
#GET("/posts")
suspend fun searchPosts(
#Query("_page") page: Int,
#Query("_limit") perPage: Int
): List<SocialNetworkPost>
}
This is my PagingSource file:
private const val STARTING_PAGE_INDEX = 1
class PagingSource(private val api: Api) : PagingSource<Int, SocialNetworkPost>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SocialNetworkPost> {
val position = params.key ?: STARTING_PAGE_INDEX
return try {
val response = api.searchPosts(position, params.loadSize)
LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
}
}
}
This is my repository:
#Singleton
class Repository #Inject constructor(private val api: Api) {
fun getPostsResults() = Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { PagingSource(api) }
).liveData
}
This is Fragment in which recyclerView is:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val postAdapter = PostsAdapter(this)
binding.apply {
recyclerView.apply {
setHasFixedSize(true)
itemAnimator = null
adapter = postAdapter.withLoadStateHeaderAndFooter(
header = PostsLoadStateAdapter { postAdapter.retry() },
footer = PostsLoadStateAdapter { postAdapter.retry() }
)
buttonRetry.setOnClickListener {
postAdapter.retry()
}
}
}
viewModel.posts.observe(viewLifecycleOwner) {
postAdapter.submitData(viewLifecycleOwner.lifecycle, it)
}
postAdapter.addLoadStateListener { loadState ->
binding.apply {
progressBar.isVisible = loadState.source.refresh is LoadState.Loading
recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading
buttonRetry.isVisible = loadState.source.refresh is LoadState.Error
textViewError.isVisible = loadState.source.refresh is LoadState.Error
// empty view
if (loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && postAdapter.itemCount < 1) {
recyclerView.isVisible = false
textViewEmpty.isVisible = true
} else {
textViewEmpty.isVisible = false
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onItemClick(post: SocialNetworkPost) {
val action = HomeFragmentDirections.actionHomeFragmentToDetailsFragment(post)
findNavController().navigate(action)
}
}
This is my repository:
class HomeViewModel #ViewModelInject constructor(private val repository: Repository) : ViewModel() {
val posts = repository.getPostsResults().cachedIn(viewModelScope)
}
This is my adapter:
class PostsAdapter(private val listener: OnItemClickListener) : PagingDataAdapter<SocialNetworkPost, PostsAdapter.PostViewHolder>(POSTS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PostViewHolder(binding)
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val currentItem = getItem(position)
if (currentItem != null) {
Log.d("PostAdapter", "onBindViewHolder: if")
holder.bind(currentItem)
} else {
Log.d("PostAdapter", "onBindViewHolder: else")
}
}
inner class PostViewHolder(private val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = getItem(position)
if (item != null) listener.onItemClick(item)
}
}
}
fun bind(post: SocialNetworkPost) {
Log.d("PostAdapter", "bind: $post")
binding.apply {
Glide.with(itemView)
.load(post.accountIcon)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.error(R.drawable.ic_account)
.into(imageViewProfilePicture)
textViewName.text = post.accountName
textViewDescription.text = post.description
Glide.with(itemView)
.load(post.descriptionImage)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.error(R.drawable.empty)
.into(imageViewDescription)
textViewLikesAmount.text = "Likes: ${post.likesAmount}"
textViewCommentsAmount.text = "Comments: ${post.commentsAmount}"
}
}
}
interface OnItemClickListener {
fun onItemClick(post: SocialNetworkPost)
}
companion object {
private val POSTS_COMPARATOR = object : DiffUtil.ItemCallback<SocialNetworkPost>() {
override fun areItemsTheSame(oldItem: SocialNetworkPost, newItem: SocialNetworkPost) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: SocialNetworkPost, newItem: SocialNetworkPost) = oldItem == newItem
}
}
}
RecyclerView needs a layoutManager, you can set it in the xml:
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
or programatically:
recyclerView.layoutManager = LinearLayoutManager(requireContext())
I have two screens first one has recycler view list of data and searchView above it that's filter data in this recycler, the view Model code of the first fragment
class MscInspectionViewModel(val activity: LaunchActivity, val mRootView: MscInspectFragment) :
BaseViewModel(),
SwipeRefreshLayout.OnRefreshListener {
val toolBarTitle: MutableLiveData<String> = MutableLiveData()
private val getDataError = MutableLiveData<Boolean>()
var listType = MutableLiveData<Int>()
val hint = MutableLiveData<String>()
private var isRefreshing: Boolean = false
private var mSharedPreferences: SharedPreferences? = null
val dataListAdapter = ContainersUnderCheckAdapter(activity)
val backClickListener = View.OnClickListener { activity.supportFragmentManager.popBackStack() }
val filterDataByTab = object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab!!.text) {
activity.resources.getString(R.string.cidPending) -> {
listType.value = 0
getPendingData()
}
activity.resources.getString(R.string.cidDone) -> {
listType.value = 1
getDoneData()
}
}
}
}
val filterData = object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
if (query.length > 2) {
val mQuery = Utility(activity).switchArabicNumerals(query)
dataListAdapter.getFilter(3, listType.value!!).filter(mQuery)
} else {
errorMessage.value = activity.resources.getString(R.string.addCorrectNumber)
}
return true
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.length > 2) {
val mQuery = Utility(activity).switchArabicNumerals(newText)
dataListAdapter.getFilter(3, listType.value!!).filter(mQuery)
}
return false;
}
}
val closeImgListener = View.OnClickListener {
mRootView.svSearchMSC.setQuery("", true)
if (listType.value == 1) {
dataListAdapter.getFilter(1, listType.value!!).filter("ANY")
} else if (listType.value == 0) {
dataListAdapter.getFilter(2, listType.value!!).filter("PENDING")
}
}
init {
listType.value = 0
mSharedPreferences = getDefaultSharedPreferences(activity.applicationContext)
toolBarTitle.value = activity.resources.getString(R.string.mscInspectTitle)
hint.value = activity.resources.getString(R.string.msc_search)
getData()
}
fun getData() {
onRetrievePostListStart()
subscription = apiAccount.getContainersUnderCheck(
"getContainersUnderCheck",
mSharedPreferences!!.getString(Constants.CFID, "")!!,
mSharedPreferences!!.getString(Constants.CFTOKEN, "")!!
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {}
.doOnTerminate {}
.subscribe({ result ->
result?.let {
if (result.ResponseCode != null && result.ResponseCode.trim() != "000") {
onRetrievePostListError(result.ResponseMessage)
} else {
result.ContainersData?.let { it1 -> onRetrievePostListSuccess(it1) }
}
}
}, { throwable ->
android.util.Log.e("getDataInquiry", throwable.message!!)
onRetrievePostListError(activity.resources.getString(R.string.general_error))
})
}
private fun getPendingData() {
val query = mRootView.svSearchMSC.query.toString()
if (query == "") {
dataListAdapter.getFilter(2, listType.value!!).filter("PENDING")
} else {
if (query.length > 2) {
dataListAdapter.getFilter(3, listType.value!!).filter(query)
} else {
errorMessage.value = activity.resources.getString(R.string.addCorrectNumber)
}
}
}
private fun getDoneData() {
val query = mRootView.svSearchMSC.query.toString()
if (query == "") {
dataListAdapter.getFilter(1, listType.value!!).filter("ANY")
} else {
if (query.length > 2) {
dataListAdapter.getFilter(3, listType.value!!).filter(query)
} else {
errorMessage.value = activity.resources.getString(R.string.addCorrectNumber)
}
}
}
private fun onRetrievePostListStart() {
loading.value = true
}
private fun onRetrievePostListFinish() {
loading.value = false
isRefreshing = false
}
private fun onRetrievePostListSuccess(containersData: List<ContainersData>) {
onRetrievePostListFinish()
dataListAdapter.updateInquiryAdapter(containersData as ArrayList<ContainersData>)
if (listType.value == 1) {
dataListAdapter.getFilter(1, listType.value!!).filter("ANY")
} else if (listType.value == 0) {
dataListAdapter.getFilter(2, listType.value!!).filter("PENDING")
}
}
private fun onRetrievePostListError(message: String?) {
onRetrievePostListFinish()
getDataError.value = true
errorMessage.value = message
}
override fun onCleared() {
super.onCleared()
subscription.dispose()
}
override fun onRefresh() {
isRefreshing = true
getData()
}
}
adapter is :
class ContainersUnderCheckAdapter(val activity: LaunchActivity) :
RecyclerView.Adapter<ContainersUnderCheckAdapter.ViewHolder>() {
private lateinit var mDataSet: ArrayList<ContainersData>
private lateinit var mDataSetFiltered: ArrayList<ContainersData>
fun updateInquiryAdapter(dataSet: ArrayList<ContainersData>) {
mDataSet = ArrayList()
mDataSet.clear()
mDataSet.addAll(dataSet)
mDataSetFiltered = mDataSet
getFilter(2, 1).filter("PENDING")
// notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: ContianerItemFieldLayoutBinding = DataBindingUtil
.inflate(
LayoutInflater.from(parent.context),
R.layout.contianer_item_field_layout,
parent,
false
)
return ViewHolder(binding, activity)
}
override fun getItemCount(): Int {
return if (::mDataSetFiltered.isInitialized) mDataSetFiltered.size else 0
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(mDataSetFiltered[position])
}
operator fun get(position: Int): ContainersData {
return mDataSetFiltered.get(position)
}
/**
* #filterType :
* IF 1 : filter on Data Type RJCTD + APPROVED
* 2 : filter on Data Type PENDING
* 3 :
*/
fun getFilter(filterType: Int, listType: Int): Filter {
return object : Filter() {
override fun performFiltering(charSequence: CharSequence): FilterResults {
val charString = charSequence.toString()
mDataSetFiltered = if (charString.isEmpty()) {
mDataSet
} else {
val filteredList = ArrayList<ContainersData>()
for (row in mDataSet) {
when (filterType) {
1 -> {
if (row.status == "RJCTD" || row.status == "APPROVED") {
filteredList.add(row)
}
}
2 -> {
if (row.status == charString) {
filteredList.add(row)
}
}
3 -> {
when (listType) {
0 -> {
if ((row.CID!!.contains(charString.toUpperCase(Locale.ROOT)) || row.TN!!.contains(
charSequence
) || row.PN!!.contains(charSequence)) && row.status == "PENDING"
) {
filteredList.add(row)
}
}
1 -> {
if ((row.CID!!.contains(charString.toUpperCase(Locale.ROOT)) || row.TN!!.contains(
charSequence
) || row.PN!!.contains(charSequence)) && row.status != "PENDING"
) {
filteredList.add(row)
}
}
}
}
}
}
filteredList
}
val filterResults = FilterResults()
filterResults.values = mDataSetFiltered
return filterResults
}
override fun publishResults(
charSequence: CharSequence,
filterResults: FilterResults
) {
if (::mDataSetFiltered.isInitialized) {
mDataSetFiltered = try {
filterResults.values as ArrayList<ContainersData>
} catch (e: Exception) {
Log.e("mDataSetFiltered",e.message!!)
ArrayList()
}
when (filterType) {
1->{
mDataSetFiltered.sortWith(Comparator { p0, p1 -> p1!!.UpdateDate.compareTo(p0!!.UpdateDate) })
}
2->{
mDataSetFiltered.sortWith(Comparator { p0, p1 -> p0!!.ID!!.compareTo(p1.ID!!) })
}
}
}
// refresh the list with filtered data
notifyDataSetChanged()
}
}
}
class ViewHolder(
private val binding: ContianerItemFieldLayoutBinding,
val activity: LaunchActivity
) : RecyclerView.ViewHolder(binding.root) {
private val viewModel = MscInspectionListViewModel(activity)
fun bind(data: ContainersData) {
viewModel.bind(data)
binding.viewModel = viewModel
}
}
}
any data in this recycler on click go to fragment has tow recycler first one to show data, the second one to pick Images
the second-page code
class MSCDataFragment : Fragment() {
lateinit var rootView: View
lateinit var activity: LaunchActivity
lateinit var utility: Utility
lateinit var loadingView: LoadingView
private lateinit var viewModel: MSCDataViewModel
private lateinit var binding: FragmentMscdataBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (getActivity() != null) {
activity = getActivity() as LaunchActivity
utility = Utility(activity)
loadingView = LoadingView(activity)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mscdata, container, false)
rootView = binding.root
initial()
return rootView
}
private fun initial() {
viewModel = ViewModelProvider(
this, ViewModelFactory(
activity,
arguments!!.getSerializable("Data") as ContainersData
)
).get(MSCDataViewModel::class.java)
binding.viewModel = viewModel
// binding.imgList.layoutManager = GridLayoutManager(activity, 3)
binding.containerInfo.layoutManager = LinearLayoutManager(activity)
binding.openCIDNotValid.typeface =
Typeface.createFromAsset(activity.assets, "Bahij_Janna-Regular.ttf")
binding.openCIDNotValid.setOnCheckedChangeListener(viewModel.onOpenCidNotValidListener)
viewModel.loading.observe(this, Observer { loading ->
loading?.let {
if (it) {
loadingView.show()
} else {
loadingView.dismiss()
}
}
})
viewModel.errorMessage.observe(this, Observer { msg ->
msg?.let {
utility.ShowToast(msg)
}
})
viewModel.imagesAdapters2.observe(this, Observer { msg ->
msg?.let {
binding.imgList.apply {
layoutManager = GridLayoutManager(activity, 3)
adapter = it
}
}
})
rootView.toolbar_Back.setOnClickListener(viewModel.backClickListener)
binding.btnAddImages.setOnClickListener(viewModel.pickImages)
binding.successContianer.setOnClickListener(viewModel.correctContainer)
binding.damagedContianer.setOnClickListener(viewModel.wrongContainer)
}
}
the view model is :
class MSCDataViewModel(val activity: LaunchActivity, val containersData: ContainersData) :
BaseViewModel(), GetImagesListener {
#Inject
lateinit var restApiAccount: RestApiAccount
val toolBarTitle: MutableLiveData<String> = MutableLiveData()
val ButtonText: MutableLiveData<String> = MutableLiveData()
var openCIDNotValidVisibility = MutableLiveData<Int>()
private val getDataError = MutableLiveData<Boolean>()
val btnImagesVisibility = MutableLiveData<Int>()
var imgeNoteVisibility = MutableLiveData<Int>()
var successVisibility = MutableLiveData<Int>()
var damagedVisibility = MutableLiveData<Int>()
var notesVisibility = MutableLiveData<Int>()
val btnVisibility = MutableLiveData<Int>()
var canNotOpen = MutableLiveData<Int>()
private val images = ArrayList<Image>()
var utility = Utility(activity)
private var CURRENTINDEX = 0
private var mSharedPreferences: SharedPreferences? = null
val DataListAdapter = ContainerDataAdapter(activity)
var imagesAdapter = ContainerImagesAdapter(activity, containersData.status!!, ArrayList())
val imagesAdapters2 = MutableLiveData<ContainerImagesAdapter2>()
val userInfo: UserInfo
val backClickListener = View.OnClickListener { activity.supportFragmentManager.popBackStack() }
val pickImages = View.OnClickListener {
pickImages()
}
val correctContainer = View.OnClickListener {}
val onOpenCidNotValidListener =
CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
successVisibility.value = View.GONE
canNotOpen.value = 1
} else {
canNotOpen.value = 0
successVisibility.value = View.VISIBLE
}
}
val wrongContainer = View.OnClickListener {}
var mscNotes: ObservableField<String> = ObservableField("")
init {
canNotOpen.value = 0
mSharedPreferences =
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
toolBarTitle.value = containersData.CID
ButtonText.value = activity.resources.getString(R.string.cleanContianer)
userInfo = utility.readObjectFromSharedPreferences(
mSharedPreferences,
Constants.USER_INFO_KEY,
UserInfo::class.java
) as UserInfo
openCIDNotValidVisibility.value = View.GONE
fillData()
}
private fun fillData() {
val data: LinkedHashMap<String, String> = containersData.data!!
val captionsMap = utility.readObjectFromSharedPreferences(
mSharedPreferences, Constants.CAPTIONS_MAP_KEY,
HashMap::class.java
) as HashMap<String, String>
if (containersData.data.size > 0) {
val list = ArrayList<KeyValueModel>()
for (inside in data.keys) {
val ky = captionsMap[inside]
val value = data[inside].toString()
ky?.let { KeyValueModel(it, value) }?.let { list.add(it) }
}
DataListAdapter.updateInquiryAdapter(list)
} else {
errorMessage.value = activity.resources.getString(R.string.no_data)
}
if (containersData.ImageList != null && containersData.ImageList.isNotEmpty()) {
imagesAdapter.updateContainerImagesAdapter(containersData.ImageList)
}
}
private fun pickImages() {
activity.setCallBack(this)
val pictureDialog: AlertDialog
val builder = activity.let { AlertDialog.Builder(it) }
val dialogView = View.inflate(activity, R.layout.choose_camera_method, null)
builder.setView(dialogView)
val nafithPopupContainer = dialogView.findViewById<RelativeLayout>(R.id.RLTitle)
nafithPopupContainer.setBackgroundColor(
ContextCompat.getColor(
activity,
R.color.mainColor
)
)
val popUpGallery = dialogView.findViewById<LinearLayout>(R.id.PopupGellary)
val popUpCamera = dialogView.findViewById<LinearLayout>(R.id.PopupCamera)
pictureDialog = builder.create()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Objects.requireNonNull<Window>(pictureDialog.window)
.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
} else {
if (pictureDialog.window != null) {
pictureDialog.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}
popUpGallery.setOnClickListener {
fromGallery()
pictureDialog.dismiss()
}
popUpCamera.setOnClickListener {
fromCamera()
pictureDialog.dismiss()
}
val popupClose = dialogView.findViewById<ImageView>(R.id.popupClose)
popupClose.setOnClickListener { pictureDialog.dismiss() }
pictureDialog.show()
}
private fun fromGallery() {
ImagePicker.create(activity)
.toolbarImageTitle(activity.resources.getString(R.string.get_image))
.toolbarArrowColor(ContextCompat.getColor(activity, R.color.colorWhite))
.showCamera(false)
.limit(6)
.start()
}
private fun fromCamera() {
ImagePicker.cameraOnly().start(activity)
}
override fun onGetImage(image: Image) {
imgeNoteVisibility.value = View.GONE
imagesAdapter.updateContainerImagesAdapter(image)
images.add(image)
}
override fun addingImagesDone(mImages: MutableList<Image>) {
images.clear()
images.addAll(mImages)
imgeNoteVisibility.value = View.GONE
val listString :ArrayList<String> = ArrayList()
for (i in mImages.indices){
listString.add(mImages[i].path)
}
imagesAdapters2.value = ContainerImagesAdapter2(activity,containersData.status!!,listString)
imagesAdapters2.value!!.notifyItemRangeChanged(0,listString.size)
}
override fun onImgDelete(image: String) {
var x = 0
try {
for (i in 0 until images.size) {
x = i
if (images[i].path == image) {
images.remove(images[i])
}
}
} catch (e: Exception) {
Log.e("errorImages", e.message!!)
Log.e("xx", x.toString())
}
}
private fun onRetrievePostListStart() {
loading.value = true
}
private fun onRetrievePostListFinish() {
loading.value = false
}
private fun onRetrievePostListSuccess(msg: String?) {
onRetrievePostListFinish()
}
private fun onRetrievePostListError(message: String?) {
onRetrievePostListFinish()
getDataError.value = true
errorMessage.value = message
}
}
Adapter code is :
class ContainerImagesAdapter2() : RecyclerView.Adapter<ContainerImagesAdapter2.ViewHolder>() {
var status: String = ""
lateinit var activity: LaunchActivity
lateinit var utility: Utility
constructor(
mActivity: LaunchActivity,
mStatus: String,
pathsList: ArrayList<String>
) : this() {
activity = mActivity
pathsDataSet = pathsList
status = mStatus
utility = Utility(activity)
}
private var pathsDataSet: ArrayList<String> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: ContianerImageFieldBinding = DataBindingUtil
.inflate(
LayoutInflater.from(parent.context),
R.layout.contianer_image_field,
parent,
false
)
return ViewHolder(binding, activity)
}
override fun getItemCount(): Int {
return pathsDataSet.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindPath(pathsDataSet[position], position)
}
inner class ViewHolder(
private val binding: ContianerImageFieldBinding,
val activity: LaunchActivity
) : RecyclerView.ViewHolder(binding.root) {
private val viewModel = MscImagesListViewModel(activity)
fun bindPath(data: String, position: Int) {
viewModel.bindPath(data)
binding.viewModel = viewModel
if (status != "PENDING") {
binding.closeImg.visibility = View.GONE
}
binding.closeImg.setOnClickListener {}
binding.mainImg.setOnClickListener {
val fragment = FullImageFragment()
val bundle = Bundle()
val list = ArrayList<String>()
for (item in 0 until pathsDataSet.size) {
list.add(pathsDataSet[item])
}
bundle.putSerializable("ImageList", list)
bundle.putInt("Position", position)
fragment.arguments = bundle
activity.supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment).addToBackStack(fragment.tag)
.commit()
}
}
}
}
if you filter data using search view in the first-page and pick images in the second page , list of picked images doesn't appear, if you going to the second page without filtering data everything ok
solve Problem found
Just Update constraint-layout library in gradle dependencies to version '2.0.0-beta4'
I have a list of user record sound
The user can click on the list and play the audio file
How can I handle the MediaPlayer and the RecyclerView item in the Seekbar as well in the MVVM correctly?
That is, when the user clicks, the visitor changes the item and updates itself, and when it is clicked on an item again, it updates itself.
I did it now, but unfortunately, to the dirtiest possible form
activity code :
class SoundListActivity : BaseActivity(), Observer<List<VoiceEntity>>, VoiceAdapter.OnClickItemListener,
OnMultiSelectVoiceListener {
private lateinit var viewModel: VoiceViewModel
private val adapter = VoiceAdapter()
private val player = MediaPlayer()
private var positionPlayItem = -1
companion object {
fun start(context: Context) {
context.startActivity(Intent(context, SoundListActivity::class.java))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sound_list)
viewModel = ViewModelProviders.of(this).get(VoiceViewModel::class.java)
viewModel.mutableList!!.observe(this, this)
adapter.onItemClickListener = this
adapter.listenerMultiSelect = this
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
fabAdd.setOnClickListener {
stopPlay()
RecordSoundActivity.start(this)
}
toolbar.setIconLeftListener(View.OnClickListener {
stopPlay()
finish()
})
}
override fun onChanged(list: List<VoiceEntity>?) {
this.progressBar.visibility = View.GONE
this.layoutEmptyState.visibility = View.INVISIBLE
this.adapter.removeAll()
if (list == null || list.isEmpty()) {
layoutEmptyState.visibility = View.VISIBLE
return
}
adapter.addItems(ArrayList(list))
}
override fun onClickItem(item: VoiceEntity, position: Int) {
if (player.isPlaying) {
player.reset()
}
if (item.isPlaying) {
item.isPlaying = false
player.reset()
adapter.notifyDataSetChanged()
return
}
this.positionPlayItem = position
adapter.items!!.forEach {
if (it != item) {
it.isPlaying = false
}
}
player.setDataSource(item.path)
player.prepare()
player.start()
item.isPlaying = true
adapter.notifyDataSetChanged()
player.setOnCompletionListener {
player.reset()
adapter.notifyItemChanged(position)
item.isPlaying = false
}
}
private fun stopPlay() {
if (positionPlayItem == -1) {
return
}
player.reset()
adapter.items!![positionPlayItem].isPlaying = false
adapter.notifyItemChanged(positionPlayItem)
}
override fun onMultiSelectVoice(items: ArrayList<VoiceEntity>) {
stopPlay()
if (items.size == 0) {
layoutSelectItem.visibility = View.GONE
return
}
txtCounterSelect.text = String.format(getString(R.string.selected_number), items.size.toString())
setStatusBarColor(R.color.black)
if (layoutSelectItem.visibility == View.GONE) {
layoutSelectItem.visibility = View.VISIBLE
}
if (items.size > 1) {
imgShare.visibility = View.GONE
imgEdit.visibility = View.GONE
} else {
imgShare.visibility = View.VISIBLE
imgEdit.visibility = View.VISIBLE
}
imgCancelSelect.setOnClickListener {
resetData()
}
imgEdit.setOnClickListener {
edit(items.first())
}
imgShare.setOnClickListener {
if (items.isEmpty()) {
return#setOnClickListener
}
shareVoice(this, items[0].path)
}
imgDelete.setOnClickListener {
val alertDialog = AlertDialog.Builder(
supportFragmentManager,
getString(R.string.note), getString(R.string.do_you_sure_delete)
)
alertDialog.setBtnNegative(getString(R.string.no), View.OnClickListener {
alertDialog.dialog!!.dismiss()
})
alertDialog.setBtnPositive(getString(R.string.yes), View.OnClickListener {
val ex = Executors.newSingleThreadExecutor()
items.forEach { item ->
viewModel.remove(item)
ex.execute { File(item.path).deleteOnExit() }
}
items.clear()
layoutSelectItem.visibility = View.GONE
setStatusBarColor(R.color.colorPrimaryDark)
alertDialog.dialog!!.dismissAllowingStateLoss()
})
alertDialog.build().show()
}
}
private fun resetData() {
adapter.itemsSelected.clear()
adapter.items?.forEach { item ->
item.statusSelect = false
}
adapter.notifyDataSetChanged()
layoutSelectItem.visibility = View.GONE
setStatusBarColor(R.color.colorPrimaryDark)
}
private fun edit(item: VoiceEntity) {
val bottomSheet = NameBottomSheet(supportFragmentManager)
bottomSheet.listener = object : NameBottomSheet.OnTitleListener {
override fun onTitle(title: String) {
item.title = title
viewModel.update(item)
resetData()
}
}
bottomSheet.item = item
bottomSheet.show()
}
override fun onBackPressed() {
if (layoutSelectItem.visibility == View.VISIBLE) {
resetData()
return
}
stopPlay()
super.onBackPressed()
}
}
adapter class code :
class VoiceAdapter : AdapterRecyclerView<VoiceEntity>() {
var onItemClickListener: OnClickItemListener? = null
var itemsSelected: ArrayList<VoiceEntity> = ArrayList()
var listenerMultiSelect: OnMultiSelectVoiceListener? = null
override fun getItemLayout(viewType: Int): Int {
return R.layout.item_voice
}
override fun onBindView(
viewDataBinding: ViewDataBinding,
viewHolder: ItemViewHolder,
position: Int,
viewType: Int,
element: VoiceEntity
) {
val binding = viewDataBinding as ItemVoiceBinding
binding.txtTitle.text = element.title
binding.txtDate.text = element.date.toAgoTime(context!!)
binding.icPlay.setImageResource(if (element.isPlaying) R.drawable.ic_pause else R.drawable.ic_play)
binding.seekBar.max = element.duration / 60
val colorSelectItem =
ContextCompat.getColor(binding.rootLayout.context, R.color.color_background_select_item_recycler_view)
val color = if (element.statusSelect) colorSelectItem else Color.TRANSPARENT
binding.rootLayout.setBackgroundColor(color)
if (element.statusSelect) {
changeColorLight(binding)
} else {
changeColorDarker(binding)
}
if (element.isPlaying) {
binding.layoutPlaying.visibility = View.VISIBLE
binding.lottieLayer.playAnimation()
//TODO : change handled voice progressBar show
val t = object : Thread() {
override fun run() {
super.run()
for (i in 0..element.duration) {
Thread.sleep(60)
binding.seekBar.progress = i
if (!element.isPlaying) break
}
}
}
t.start()
} else {
binding.layoutPlaying.visibility = View.GONE
binding.lottieLayer.cancelAnimation()
}
binding.rootLayout.setOnClickListener {
if (itemsSelected.size > 0) {
val item = items!![viewHolder.adapterPosition]
if (itemsSelected.contains(item)) {
item.statusSelect = false
itemsSelected.remove(item)
binding.rootLayout.animatedColorBackgroundSelected(false)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
changeColorDarker(binding)
return#setOnClickListener
}
item.statusSelect = true
itemsSelected.add(item)
binding.rootLayout.animatedColorBackgroundSelected()
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
changeColorLight(binding)
return#setOnClickListener
}
onItemClickListener?.onClickItem(element, position)!!
}
binding.rootLayout.setOnLongClickListener {
val item = items!![viewHolder.adapterPosition]
if (itemsSelected.contains(item)) {
item.statusSelect = false
itemsSelected.remove(item)
binding.rootLayout.animatedColorBackgroundSelected(false)
changeColorDarker(binding)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
}
item.statusSelect = true
itemsSelected.add(item)
binding.rootLayout.animatedColorBackgroundSelected()
changeColorLight(binding)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
true
}
}
private fun changeColorLight(binding: ItemVoiceBinding) {
binding.txtDate.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_subtitle_light))
binding.txtTitle.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_title_light))
}
private fun changeColorDarker(binding: ItemVoiceBinding) {
binding.txtDate.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_subtitle))
binding.txtTitle.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_title))
}
interface OnClickItemListener {
fun onClickItem(item: VoiceEntity, position: Int)
}
}
github repository (open source project)