Show BottomSheetDialogFragment when Click BottomNavigationView - android

I am new to Android Development. I like to show Bottom Sheet Dialog Fragment when I click one of the menu buttons at the Bottom Navigation View. When I click the favorite button on the languages, it shows empty for the bottom sheet dialog. Is there other way to do it? Thanks.
Main Activity
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
replacementFragment(HomeFragment())
binding.bottomNavigationView.setOnItemSelectedListener {
when (it.itemId) {
R.id.home -> replacementFragment(HomeFragment())
R.id.language -> replacementFragment(LanguageFragment())
// I cannot show the list on the fragment
R.id.favourite -> showFavouriteBottomFragment()
}
true
}
// val bottomSheetFragment = FavouriteFragment()
// bottomSheetFragment.show(supportFragmentManager, bottomSheetFragment.getTag() )
}
private fun replacementFragment(fragment: Fragment) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentContainerView, fragment)
fragmentTransaction.commit()
}
// I cannot get the following part to work....
private fun showFavouriteBottomFragment() {
val bottomSheetFragment = FavouriteFragment()
bottomSheetFragment.show(supportFragmentManager, bottomSheetFragment.getTag() )
}
}
Favourite Fragment
#AndroidEntryPoint
class FavouriteFragment: BottomSheetDialogFragment(){
private var _binding: FavouriteBottomSheetBinding ?= null
private val binding get() = _binding!!
private val viewModel: FavouriteViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FavouriteBottomSheetBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// val binding = FavouriteBottomSheetBinding.bind(view)
val favouriteAdapter = FavouriteAdapter()
binding.apply {
favouriteLanguageList.apply{
adapter = favouriteAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
}
}
viewModel.favouriteLanguage.observe(viewLifecycleOwner){
favouriteAdapter.submitList(it)
}
}
}
Bottom Menu
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/home"
android:icon="#drawable/ic_baseline_home_24"
android:title="Home"
tools:ignore="HardcodedText" />
<item
android:id="#+id/favourite"
android:icon="#drawable/ic_baseline_favorite_24"
android:title="Favourite"
tools:ignore="HardcodedText" />
<item
android:id="#+id/language"
android:icon="#drawable/ic_baseline_language_24"
android:title="Language"
tools:ignore="HardcodedText" />
</menu>
AppModule
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
fun provideTestString() = "This is a string we will inject"
#Provides
#Singleton
fun provideDatabase(
app: Application,
callback: LanguageDatabase.Callback
) = Room.databaseBuilder(app, LanguageDatabase::class.java, "language_database")
.fallbackToDestructiveMigration()
.addCallback(callback)
.build()
#Provides
fun provideLanguageDao(db: LanguageDatabase) = db.languageDao()
#ApplicationScope
#Provides
#Singleton
fun provideApplicationScope() = CoroutineScope(SupervisorJob())
}
#Retention(AnnotationRetention.RUNTIME)
#Qualifier
annotation class ApplicationScope
Favourite Adapter
class FavouriteAdapter : ListAdapter<Language, FavouriteAdapter.FavouriteViewAHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavouriteViewAHolder {
val binding = ItemFavouriteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return FavouriteViewAHolder(binding)
}
override fun onBindViewHolder(holder: FavouriteViewAHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
inner class FavouriteViewAHolder(private val binding: ItemFavouriteBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(language: Language){
binding.apply {
rbIsClicked.isChecked = language.isChecked
tvFavouriteLanguage.text = language.language
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Language>() {
override fun areItemsTheSame(oldItem: Language, newItem: Language) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Language, newItem: Language) = oldItem == newItem
}
}
Favourite View Model
#HiltViewModel
class FavouriteViewModel #Inject constructor(
private val languageDao: LanguageDao,
) : ViewModel() {
val favouriteLanguage = languageDao.getFavouriteLanguageByName().asLiveData()
}
Preferences Manager
private const val TAG = "PreferencesManager"
private val Context.dataStore by preferencesDataStore("user_preferences")
enum class SortOrder { BY_NAME}
data class FilterPreferences(val sortOrder: SortOrder, val hideSelectedLanguage: Boolean, val selectedLanguage: String)
#Singleton
class PreferencesManager #Inject constructor(#ApplicationContext context: Context){
private val dataStore = context.dataStore
val preferencesFlow = dataStore.data
.catch { exception ->
if(exception is IOException){
Log.e(TAG, "Error reading preferences", exception)
emit(emptyPreferences())
}else{
throw exception
}
}
.map{ preferences ->
val sortOrder = SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?:SortOrder.BY_NAME.name
)
val hideSelectedLanguage = preferences[PreferencesKeys.HIDE_SELECTED_LANGUAGE] ?: false
val selectedLanguage = preferences[PreferencesKeys.SELECTED_LANGUAGE]?: "English"
FilterPreferences(sortOrder, hideSelectedLanguage, selectedLanguage)
}
suspend fun updateSortOrder(sortOrder: SortOrder){
dataStore.edit { preferences ->
preferences[PreferencesKeys.SORT_ORDER] = sortOrder.name
}
}
suspend fun updateHideSelectedLanguage(hideSelectedLanguage: Boolean){
dataStore.edit { preferences ->
preferences[PreferencesKeys.HIDE_SELECTED_LANGUAGE] = hideSelectedLanguage
}
}
suspend fun updateSelectedLanguage(selectedLanguage: String){
dataStore.edit{ preferences ->
preferences[PreferencesKeys.SELECTED_LANGUAGE] = selectedLanguage
}
}
private object PreferencesKeys{
val SORT_ORDER = stringPreferencesKey("sort_order")
val HIDE_SELECTED_LANGUAGE = booleanPreferencesKey("hide_selected_language")
val SELECTED_LANGUAGE = stringPreferencesKey("selected_language")
}
}
I have a Language Fragment which looks similar to Favourite Fragment.

I find a temporary solution...but I still have a bug as I have the press the favourite button twice to show a list in the BottomSheetDialogFragment.
Is there a way, I can solve the problem? The first time I press the favourite button it doesn't shows a BottomSheetDialog fragment.... I have to press it again to show the list.
KC
In the MainActivity, I declare the class favouriteFragment.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// declare the favroute fragment.
val favouriteFragment = FavouriteFragment()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
replacementFragment(HomeFragment())
binding.bottomNavigationView.setOnItemSelectedListener {
when (it.itemId) {
R.id.home -> replacementFragment(HomeFragment())
R.id.language -> replacementFragment(LanguageFragment())
R.id.favourite ->
// Pass the instace in the function.
showFavouriteBottomFragment(favouriteFragment)
}
true
}
In the function in Main Activity
private fun showFavouriteBottomFragment(favouriteFragment: FavouriteFragment) {
favouriteFragment.show(supportFragmentManager, favouriteFragment.tag)
}

I remove "setHasFixedSize(true)" and it can load the first time when I press the favorite button.

Related

My recyclerview resets when I add or delete an item from a Table (with Room) in Kotlin

First of all, I am Spanish so my english is not good.
I have an app with Kotlin and room, and it has a Recyclerview.
I have 3 tables: coaster, user and favorite.
The user can add coasters to favorite, and this is done succesfully.
The problem that I have is that when the user clicks on the button to add or delete from favorites, the recyclerview resets, it displays again. So it scrolls to the top of the Screen, and also some odd spaces appears after the element.
I also have a function to search, and it happens the same: spaces appears after each element when I am searching.
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
My main activity:
class CoasterFragment : Fragment() {
lateinit var coasterListener: CoasterListener
lateinit var usuarioCoaster: List\<UsuarioCoaster\>
private lateinit var searchView: SearchView
private lateinit var cAdapter: CoasterRecyclerViewAdapter
private var \_binding: FragmentCoasterBinding? = null
private val binding get() = \_binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
/* val livedata = viewModel.coasters()
livedata.observe(viewLifecycleOwner,object: Observer <List<CoasterFavorito>> {
override fun onChanged(it: List<CoasterFavorito>) {
createRecyclerView(it)
livedata.removeObserver(this)
}
})*/
viewModel.coasters().observe(viewLifecycleOwner){createRecyclerView(it)}
coasterListener = CoasterListenerImpl(requireContext(), viewModel)
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun createRecyclerView(coasters: List<CoasterFavorito>) {
cAdapter =
CoasterRecyclerViewAdapter(
coasters as MutableList<CoasterFavorito>,
coasterListener,
requireContext()
)
val recyclerView = binding.recyclerCoaster
recyclerView.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = cAdapter
addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
cAdapter.notifyDataSetChanged()
}
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { createRecyclerView(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
my adapter:
class CoasterRecyclerViewAdapter(val coasters: List<CoasterFavorito>, val listener: CoasterListener,
val context: Context, ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
fun relleno(data: CoasterFavorito){
binding.nombre.text = data.coaster.nombre
binding.parque.text = data.coaster.parque
binding.ciudad.text = data.coaster.ciudad
binding.provincia.text = data.coaster.provincia
binding.comunidad.text = data.coaster.comunidadAutonoma
Glide
.with(context)
.load(data.coaster.imagen)
.centerCrop()
.into(binding.imagen)
binding.check.isChecked = data.favorito
binding.check.setOnClickListener{
if (data.favorito) {
listener.delFavorito(data.coaster.id)
binding.check.isChecked = false
} else {
listener.addFavorito(data.coaster.id)
binding.check.isChecked = true
}
}
}
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, adapter: CoasterRecyclerViewAdapter, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, this, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.relleno(coasters[position])
override fun getItemCount() = coasters.size
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
Your createRecyclerView function should be invoked only once in a whole lifecycle of the Fragment. You should not create any new RecyclerView.Adapter, or set a LayoutManager to the RecyclerView every time your data set changes.
Therefore the Observer used in viewModel.coasters.observe() should only submit a new List to the existing Adapter and call .notifyDataSetChanged(), or other notifying functions.

Shared ViewModel Not Working With Bottom Sheet Dialog Fragment, DB and UI

i have a really simple vocabulary note app contains 2 fragment and 1 root activity. In HomeFragment i have a button "addVocabularyButton". When it is clicked a BottomSheetDialogFragment appears and user gives 3 inputs and with a viewmodel it is saved in DB. My problem is when i save the input to the DB it works fine but i cannot see in HomeFragment that word instantaneously. I have to re-run the app to see in home fragment. I am using Navigation library and recycler view in home fragment.
Github link : https://github.com/ugursnr/MyVocabularyNotebook
Home Fragment
class HomeFragment : Fragment() {
private var _binding : FragmentHomeBinding? = null
private val binding get() = _binding!!
private var vocabularyAdapter = VocabulariesHomeAdapter()
private lateinit var sharedViewModel: AddVocabularySharedViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(layoutInflater,container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
sharedViewModel.getAllVocabulariesFromDB()
observeAllVocabularies()
prepareRecyclerView()
addVocabularyOnClick()
vocabularyAdapter.onItemDeleteClicked = {
sharedViewModel.deleteVocabulary(it)
observeAllVocabularies()
}
}
private fun prepareRecyclerView(){
binding.recyclerViewHome.apply {
layoutManager = LinearLayoutManager(context)
adapter = vocabularyAdapter
}
}
private fun addVocabularyOnClick(){
binding.addVocabularyButton.setOnClickListener{
val action = HomeFragmentDirections.actionHomeFragmentToAddVocabularyBottomSheetFragment()
Navigation.findNavController(it).navigate(action)
}
}
private fun observeAllVocabularies(){
sharedViewModel.allVocabulariesLiveData.observe(viewLifecycleOwner, Observer {
vocabularyAdapter.updateVocabularyList(it)
})
}
}
Dialog Fragment
class AddVocabularyBottomSheetFragment : BottomSheetDialogFragment() {
private var _binding : FragmentAddVocabularyBottomSheetBinding? = null
private val binding get() = _binding!!
private lateinit var sharedViewModel: AddVocabularySharedViewModel
private var vocabularyInput : String? = null
private var translationInput : String? = null
private var sampleSentenceInput : String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddVocabularyBottomSheetBinding.inflate(layoutInflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
binding.addOrUpdateVocabularyButton.setOnClickListener {
vocabularyInput = binding.vocabularyActualET.text.toString()
translationInput = binding.vocabularyTranslationET.text.toString()
sampleSentenceInput = binding.vocabularySampleSentenceET.text.toString()
val inputVocabulary = Vocabulary(vocabularyInput,translationInput,sampleSentenceInput)
insertVocabularyToDB(inputVocabulary)
sharedViewModel.getAllVocabulariesFromDB()
dismiss()
}
}
private fun insertVocabularyToDB(vocabulary: Vocabulary){
sharedViewModel.insertVocabulary(vocabulary)
}
}
Shared ViewModel
class AddVocabularySharedViewModel(application: Application) : AndroidViewModel(application) {
private var _allVocabulariesLiveData = MutableLiveData<List<Vocabulary>>()
private var _vocabularyLiveData = MutableLiveData<Vocabulary>()
val allVocabulariesLiveData get() = _allVocabulariesLiveData
val vocabularyLiveData get() = _vocabularyLiveData
val dao = VocabularyDatabase.makeDatabase(application).vocabularyDao()
val repository = VocabularyRepository(dao)
fun insertVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.insertVocabulary(vocabulary)
}
fun updateVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.updateVocabulary(vocabulary)
}
fun deleteVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.deleteVocabulary(vocabulary)
}
fun getAllVocabulariesFromDB() = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getAllVocabulariesFromDB()
withContext(Dispatchers.Main){
_allVocabulariesLiveData.value = temp
}
}
fun getVocabularyDetailsByID(vocabularyID : Int) = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getVocabularyDetailsByID(vocabularyID).first()
withContext(Dispatchers.Main){
_vocabularyLiveData.value = temp
}
}
}
Adapter
class VocabulariesHomeAdapter : RecyclerView.Adapter<VocabulariesHomeAdapter.VocabulariesHomeViewHolder>() {
lateinit var onItemDeleteClicked : ((Vocabulary) -> Unit)
val allVocabulariesList = arrayListOf<Vocabulary>()
class VocabulariesHomeViewHolder(val binding : RecyclerRowBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VocabulariesHomeViewHolder {
return VocabulariesHomeViewHolder(RecyclerRowBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: VocabulariesHomeViewHolder, position: Int) {
val vocabulary = allVocabulariesList[position]
holder.binding.apply {
actualWordTV.text = vocabulary.vocabulary
translationWordTV.text = vocabulary.vocabularyTranslation
deleteButtonRV.setOnClickListener {
onItemDeleteClicked.invoke(vocabulary)
notifyItemRemoved(position)
}
}
}
override fun getItemCount(): Int {
return allVocabulariesList.size
}
fun updateVocabularyList(newList : List<Vocabulary>){
allVocabulariesList.clear()
allVocabulariesList.addAll(newList)
notifyDataSetChanged()
}
}
I know there are lots of codes up there but i have a really big problems about using these dialog fragments. Thank you for your help.
This is because multiple instances of the same View Model are created by the Navigation Library for each Navigation Screen.
You need to tell the Navigation Library to share the same ViewModel between all navigation screens.
Easiest way to fix this is to scope the viewModel to the Activity rather than a Fragment and using it in all your fragments.
val viewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
This way, the viewModel is scoped to the Application instance rather than Fragment. This will keep the state in the viewModel persistent across the Application.
You can also do this by scoping the viewModel to the navigation graph.
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
Alternate method, if you're using dependency injection libraries
val navController = findNavController();
val navBackStackEntry = navController.currentBackStackEntry!!
If you use hilt, you can just pass your NavBackStackEntry of the NavGraph to hiltViewModel()
val viewModel = hiltViewModel<MyViewModel>(//pass NavBackStackEntry)
This will give you a viewModel that is scoped to NavBackStackEntry and will only be recreated when you pop the NavBackStackEntry(ie Navigate out of the navigation screens.)

Handle android rotate screen

How to handle android rotate screen if I have a fragment that observe the data from view model to fill recyclerview list ?!
when rotation happened the data lost, so how can I handle it with viewmodel so the user didn't feel any different after rotation?
this is the part of the view model the return data:
private fun getImageList(keyWord: String) {
responseManager.loading()
val disposable = imageListUseCase.execute(keyWord, { success ->
responseManager.hideLoading()
_observeImageListData.value = Event(success)
}, { error ->
responseManager.failed(error)
})
compositeDisposable.add(disposable)
}
and here I set the recycler data on the fragment:
UPDATE
#AndroidEntryPoint
class ImagePickerFragment : Fragment() {
private lateinit var imagePickerBinding: FragmentImagePickerBinding
private val imagePickerViewModel: ImagePickerViewModel by activityViewModels()
private lateinit var imagePickerAdapter: ImagePickerAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
imagePickerBinding = FragmentImagePickerBinding.inflate(inflater, container, false)
observeImageList()
observeSearchText()
return imagePickerBinding.root
}
private fun observeImageList() {
imagePickerViewModel.observeImageListData.observe(
viewLifecycleOwner,
EventObserver { imageList ->
imagePickerAdapter = ImagePickerAdapter(imageList, imagePickerViewModel)
imagePickerBinding.apply {
rvImageList.setHasFixedSize(true)
rvImageList.layoutManager = GridLayoutManager(requireContext(),2)
rvImageList.adapter = imagePickerAdapter
}
})
}
Any tips?
UPDATE :
The viewModel extends from BaseViewModel :
abstract class BaseViewModel: ViewModel() {
val compositeDisposable = CompositeDisposable()
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
and this is the full viewmodel:
#HiltViewModel
class ImagePickerViewModel #Inject constructor(
private val imageListUseCase: ImageListUseCase,
private val responseManager: ResponseManager
) : BaseViewModel() {
private val _observeImageListData = MutableLiveData<Event<ArrayList<Image>>>()
init {
getImageList(Constants.KEY_WORD)
}
private fun getImageList(keyWord: String) {
responseManager.loading()
val disposable = imageListUseCase.execute(keyWord, { success ->
responseManager.hideLoading()
_observeImageListData.value = Event(success)
}, { error ->
responseManager.failed(error)
})
compositeDisposable.add(disposable)
}
fun filterSearchKeyWord(filteredKeyWord: String) {
if(filteredKeyWord.isNotEmpty())
getImageList(filteredKeyWord)
}
//getters:
val observeImageListData: LiveData<Event<ArrayList<Image>>>
get() = _observeImageListData
}
In your manifest, try putting this in your activity tag:
android:configChanges="orientation"
So it looks like:
<activity
android:name=".MainActivity"
android:configChanges="orientation"
android:exported="true">

Textview dispalys "androidx.livecycle.CoroutineLiveData"

I want to display the sum of a column from my room database in a textview.
After implementing my Query and setting the text of the Textview, it displays the following:
androidx.lifecycle.CoroutineLiveData#76f6edd
Any idea why this happens? Is it because my Query does not work or my way of implementign it is wrong?
It is my first time ever of actually programming. I know probably most of my code is not 100% correct, but I am working on it.
The Query from my Dao:
#Query("SELECT SUM(total)AS sum_total FROM receipt_table")
fun getSum(): Flow<Float>
The Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
}
totalSumTextView.apply {
val totalSum = viewModel.totalSum
text = totalSum.toString()
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
And the Viewmodel:
#HiltViewModel
class PurchaseViewmodel #Inject constructor(
private val receiptDao: ReceiptDao
): ViewModel() {
private val tasksEventChannel = Channel<TasksEvent>()
val addTaskEvent = tasksEventChannel.receiveAsFlow()
val receipts = receiptDao.getAllReceipts().asLiveData()
val totalSum = receiptDao.getSum().asLiveData()
fun onAddResult(result: Int){
when (result){
ADD_RECEIPT_RESULT_OK ->showReceiptSavedConfirmation("Receipt has been saved")
}
}
private fun showReceiptSavedConfirmation (text: String) = viewModelScope.launch {
tasksEventChannel.send(TasksEvent.ShowReceiptSavedConfirmation(text))
}
fun onSwipe (receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
tasksEventChannel.send(TasksEvent.ShowUndoDelete(receipts))
}
fun unDoDeleteClick (receipts: Receipts) = viewModelScope.launch {
receiptDao.insert(receipts)
}
sealed class TasksEvent {
data class ShowReceiptSavedConfirmation(val msg: String) : TasksEvent()
data class ShowUndoDelete(val receipts: Receipts) : TasksEvent()
}
}
You are getting a LiveData object here, not just a value:
val totalSum = receiptDao.getSum().asLiveData()
When you call:
totalSum.toString()
It calls toString method on the LiveData object that the reason, why you have "androidx.lifecycle.CoroutineLiveData#76f6edd" inside your TextView.
To fix the issue, just replace:
totalSumTextView.apply {
val totalSum = viewModel.totalSum
text = totalSum.toString()
}
with:
viewModel.totalSum.observe(this) { totalSumTextView.text = it}
Detailed review, how to work with a LiveData you can find here:
https://developer.android.com/topic/libraries/architecture/livedata

Can I use one instead of my two view models?

Can I use one ViewModel instead of my two view models(AboutViewModel and AboutListItemViewModel)?
Here is my code:
class AboutAdapter(private val clickListener: OnItemClickListener?) : AbstractListAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AboutItemViewHolder {
val binding: AboutBinding = DataBindingUtil
.inflate(
LayoutInflater.from(parent.context), R.layout.view_item_about,
parent, false
)
return AboutItemViewHolder(binding = binding)
}
override fun onBindHolder(holder: RecyclerView.ViewHolder, position: Int, item: Any) {
(holder as AboutItemViewHolder).bind(item as SettingsItemViewModel, listener = clickListener)
}
}
My AboutFragment:
class AboutFragment : BaseFragment() {
private lateinit var viewModel: AboutViewModel
private lateinit var viewModelFactory: AboutViewModelFactory
private var onItemClickListener: OnItemClickListener = object : OnItemClickListener {
override fun onItemClick(titleName: Int) {
when (titleName) {
R.string.about_terms_service -> {
activity?.addFragment(WebViewFragment.newInstance(TERMS_LINK, getString(R.string.about_terms_service)))
}
R.string.about_open_source_licenses -> activity?.addFragment(LicensesFragment())
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModelFactory = AboutViewModelFactory(requireContext(), onItemClickListener)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(AboutViewModel::class.java)
return inflater.inflate(R.layout.fragment_base_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activityComponents?.updateAppbarTitleWithFabAction(getString(R.string.about_feature_title))
setupViews()
}
private fun setupViews() {
try {
val layoutManager = LinearLayoutManager(activity)
recyclerView?.layoutManager = layoutManager
val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
ContextCompat.getDrawable(context ?: return, R.drawable.shape_full_divider)?.let {
dividerItemDecoration.setDrawable(it)
}
recyclerView?.addItemDecoration(dividerItemDecoration)
recyclerView?.adapter = AboutAdapter(viewModel.onItemClickListener).apply {
data = viewModel.getListItems()
}
} catch (e: Exception) {
e.log()
}
}
}
My AboutItemViewHolder:
class AboutItemViewHolder(
binding: AboutBinding
) : RecyclerView.ViewHolder(binding.root){
private var aboutBinding: AboutBinding? = null
init {
this.aboutBinding = binding
}
fun bind(item: SettingsItemViewModel, listener: OnItemClickListener?) {
aboutBinding?.about = AboutListItemViewModel(item)
aboutBinding?.onItemClickListener = listener
aboutBinding?.executePendingBindings()
}
}
interface OnItemClickListener {
fun onItemClick(titleName: Int)
}
My first ViewModel which I used in adapter AboutListItemViewModel:
class AboutListItemViewModel(item: SettingsItemViewModel) {
val titleName: Int = item.titleId
val subTitleName: String? = item.value
var isVisible: Boolean = item.value != null
}
My second ViewModel which I used in fragment AboutViewModel:
class AboutViewModel(val appContext: Context, val onItemClickListener: OnItemClickListener): ViewModel() {
fun getListItems(): List<SettingsItemViewModel> {
return listOf(
SettingsItemViewModel(
titleId = R.string.about_app_version,
value = AppTools.getAppVersionName(appContext)
),
SettingsItemViewModel(
titleId = R.string.about_copyright,
value = appContext.getString(R.string.about_copyright_description)
),
SettingsItemViewModel(
titleId = R.string.about_terms_service,
itemIsClickable = true
),
SettingsItemViewModel(
titleId = R.string.about_open_source_licenses,
itemIsClickable = true
)
)
}
}
My AboutViewModelFactory:
class AboutViewModelFactory(val appContext: Context, private val onItemClickListener: OnItemClickListener) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AboutViewModel::class.java)) {
return AboutViewModel(appContext, onItemClickListener) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
SettingsViewModel is simple data class:
data class SettingsItemViewModel(
#StringRes val titleId: Int,
val value: String? = null,
val switchEnabled: Boolean? = null,
val isChecked: Boolean = false,
val itemIsClickable: Boolean = false,
val colorResId: Int = R.color.black
)
ViewModels are for the MVVM pattern, in which a ViewModel is ignorant of any View type classes, such as an OnItemClickListener, so you have an unintended usage where you are passing listeners to ViewModel factories. This also will leak your Fragment or Activity if there is a configuration change.
I don't see you even using the listener from within the ViewModel, so you could completely eliminate this property and the factory altogether. You can inherit from AndroidViewModel and take a single Context argument in your constructor. Then you can use by viewModels() for your ViewModel property.
Without these view callback arguments, there would be nothing to limit you from putting all of this in a single ViewModel implementation.
It's also odd that your view model has a function that returns a list of other ViewModels. Why can these just be simple data classes? A ViewModel is shouldn't be directly instantiated yourself (except by a factory implementation). Otherwise, its lifecycle won't be managed.

Categories

Resources