Search code examples
androidkotlinandroid-fragmentsandroid-recyclerviewandroid-gridlayout

Change RecyclerView Layout (from Linear to Grid and reverse) when rotate device automatically


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


Solution

  • 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

    1. 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>
    
    1. 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
        }
    
    
    1. 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)
            }
        }
    
    1. 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()
        }