Can we pass arguments to Fragments more beautifully? - android

For every Fragment class I make, I add something like this:
companion object {
private const val PARAMETER_1 = "parameter1"
private const val PARAMETER_2 = "parameter2"
fun newInstance(parameter1: String, parameter2: Int) = MyDialog().apply {
arguments = bundleOf(
PARAMETER_1 to parameter1,
PARAMETER_2 to parameter2)
}
}
And then I add:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = arguments ?: return
property1 = args[PARAMETER_1]
property2 = args[PARAMETER_2]
}
This isn't horrific. But it is boilerplate that it would be great to get rid of.
Here's my attempt so far:
abstract class BaseFragment : Fragment() {
abstract val constructorArguments: List<KMutableProperty<*>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = arguments ?: return
constructorArguments.forEach {
val key = keyPrefix + it.name
val argument = args.get(key)
val clazz = it.javaClass
val typedArgument = clazz.cast(argument)
it.setter.call(typedArgument)
}
}
companion object {
const val keyPrefix = "ARGUMENT_"
fun newInstance(fragment: BaseFragment, vararg parameters: Any): BaseFragment {
val constructorArguments = fragment.constructorArguments
val parameterMap = mutableListOf<Pair<String, Any?>>()
constructorArguments.forEachIndexed { index, kMutableProperty ->
val key = keyPrefix + kMutableProperty.name
val parameter = parameters[index]
parameterMap.add(Pair(key, parameter))
}
val args = bundleOf(*parameterMap.toTypedArray())
fragment.arguments = args
return fragment
}
}
}
And then, in the actual fragment I can just have:
class MyFragment : BaseFragment() {
lateinit var myProperty: String
override val constructorArguments = listOf<KMutableProperty<*>>(
::myProperty
)
companion object {
fun newInstance(argument: String) = BaseFragment.newInstance(MyFragment(), argument)
}
}
This approach is far from perfect - especially the:
val parameter = parameters[index]
Does anyone know a better way to do this? Do you have some suggestions for how my approach can be improved? Or is this whole idea doomed to fail, and have I wasted a morning?

An 'answer' to this question is to use the Android Jetpack Navigation library. It provides SafeArgs, which greatly simplifies passing arguments to Fragments. See:
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args

You can have a base fragment that defines a common args parameter
abstract class BaseFragment : Fragment() {
companion object {
const val ARGS_KEY = "__ARGS__"
}
fun <T: Parcelable> getArgs(): T = requireArguments().getParcelable(ARGS_KEY)
fun putArgs(args: Parcelable): Bundle = (arguments ?: Bundle()).apply {
putParcelable(ARGS_KEY, args)
}
}
Then
#Parcelize data class Args(val parameter1: String, val parameter2: Int)
companion object {
fun newInstance(args: Args) = MyDialog().apply {
putArgs(args)
}
}
And now you can do it like
class MyFragment: BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val args: Args = getArgs()
args.parameter2
}
}

Related

Why is view null despite called in viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) in viewPager2's fragment?

We have a random crash on production in this class when accessing the binding at line 10 :
class BulletinFragment : Fragment(R.layout.fragment_bulletins) {
private val bulletinViewModel: BulletinsViewModel by viewModel()
private val binding by viewBinding(FragmentBulletinsBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
bulletinViewModel.switchState.collect {
binding.bulletinLiveNotificationsBanner.switch.isSelected = it
}
}
}
}
}
viewModel is provided by Koin, and the binding delegate is Zhuiden's one from here
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var _binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_binding = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = _binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { _binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
}
This fragment is called within a viewPager2:
class CartPagerAdapter(fragment: Fragment) : FragmentStateAdapter(
fragment.childFragmentManager,
fragment.viewLifecycleOwner.lifecycle
) {
val firstFragment = FirstFragment()
val secondFragment = SecondFragment()
override fun createFragment(position: Int): Fragment = when (position) {
Tab.FIRST_TAB.tabIndex -> firstFragment
Tab.SECOND_TAB.tabIndex -> secondFragment
Tab.THIRD_TAB.tabIndex -> BulletinFragment()
else -> error("The fragment position should in 0 < x < 2 but was '$position'")
}
fun handleDeeplink(deeplink: Uri) {
when (deeplink.host) {
FIRST_TAB_DEEPLINK_HOST -> firstFragment.handleDeeplink(deeplink)
SECOND_TAB_DEEPLINK_HOST -> secondFragment.handleDeeplink(deeplink)
}
}
override fun getItemCount(): Int = 3
}
class CartHomeFragment : Fragment(R.layout.fragment_cart_home), CartHomeContract.View {
private var tabLayoutMediator: TabLayoutMediator? = null
private val args: CartHomeFragmentArgs by navArgs()
private var initTab: Int? = null
// betSlip needs to scroll to top when displaying QrCodes tab (set when moving to QrCode tab after validating cart)
private val pagerAdapter: CartPagerAdapter by adapter { CartPagerAdapter(this) }
private val binding by viewBinding(FragmentCartHomeBinding::bind)
private val scope
get() = viewLifecycleOwner.lifecycleScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initTab = args.tabIndex
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
override fun onDestroyView() {
tabLayoutMediator?.detach()
tabLayoutMediator = null
super.onDestroyView()
}
private fun initialize() {
scope.launch {
binding.fragmentCartHomeViewPager.run {
adapter = pagerAdapter
setPagerCurrentItem(this)
offscreenPageLimit = 2
}
tabLayoutMediator = TabLayoutMediator(binding.cartTabLayout, binding.fragmentCartHomeViewPager) { tab, position ->
val (title, contentDesc) = with(getTab(position)) { getString(title) to getString(contentDesc) }
tab.text = title
tab.contentDescription = contentDesc
}
tabLayoutMediator?.attach()
}
}
private fun setPagerCurrentItem(viewPager: ViewPager2) {
val initialIntent = arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)
initialIntent?.data?.let {
pagerAdapter.handleDeeplink(it)
viewPager.setCurrentItemForDeeplink(it.host)
initialIntent.data = null
} ?: run {
initTab?.let {
viewPager.setCurrentItem(it, false)
initTab = null
}
}
}
fun getTab(index: Int): Tab {
return Tab.values()[index]
}
private fun ViewPager2.setCurrentItemForDeeplink(deeplink: String?) {
setCurrentItem(Tab.getTabIndexForDeeplink(deeplink), false)
}
companion object {
const val FIRST_TAB_DEEPLINK_HOST = "first"
const val SECOND_TAB_DEEPLINK_HOST = "second"
const val THIRD_TAB_DEEPLINK_HOST = "third"
val DEFAULT_TAB: Tab = Tab.FIRST_TAB
const val SECOND_TAB_DEEPLINK_DETAILS_PATH = "/details"
}
}
enum class Tab(#StringRes val title: Int, #StringRes val contentDesc: Int, val deeplink: String) {
FIRST_TAB(R.string.first_tab_tab_title, R.string.a11y_first_tab, FIRST_TAB_DEEPLINK_HOST),
SECOND_TAB(R.string.second_tab_title, R.string.a11y_second_tab, SECOND_TAB_DEEPLINK_HOST),
THIRD_TAB(R.string.third_tab_title, R.string.a11y_third_tab, THIRD_TAB_DEEPLINK_HOST);
val tabIndex: Int = ordinal
companion object {
fun getTabIndexForDeeplink(deeplink: String?): Int =
(values().firstOrNull { it.deeplink == deeplink }
?: DEFAULT_TAB)
.tabIndex
}
}
In the BulletinFragment, I know that the repeatOnLifecycle block seems useless here but we need it for some reason that is not necessary to explain here. I just would like to understand what is wrong with this piece of code. Actually, we get from crashlytics the following crash happening randomly (rare enough to not succeed to reproduce it, but frequent enough to significantly decrease the crashfree):
Fatal Exception: java.lang.IllegalStateException Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
androidx.fragment.app.Fragment.getViewLifecycleOwner (Fragment.java:377)
com.mycompany.myapp.common.tools.FragmentViewBindingDelegate.getValue (FragmentViewBindingDelegate.kt:40)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.<clinit> (BulletinFragment.kt:18)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.access$getBinding (BulletinFragment.java:15)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:25)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:24)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2.emit (Emitters.kt:227)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2$1.invokeSuspend (Emitters.kt:12)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.internal.DispatchedContinuation.resumeWith (DispatchedContinuation.kt:205)
kotlin.coroutines.SafeContinuation.resumeWith (SafeContinuationJvm.kt:41)
How can we endup with this crash when
we tie the coroutine with the viewLifecycleOwner lifecycleScope
and the collect is done inside a block where the lifecycleOwner state is STARTED, the lifecycleOwner being the view if I properly understand.
How the view can be null in this case ??? Is it related to the ViewPager2

Kotlin DAO return null when try to access

I am new to Android development. When I try to access the id from the Room. I get null. Is there something I did wrong?
Language
#Entity(tableName = "language_table6")
data class Language(
val isChecked: Boolean = false,
val language: String,
val isFavourite: Int = 0,
#PrimaryKey(autoGenerate = true) val id: Int = 0
)
LanguageDao
#Query("SELECT id FROM language_table6 WHERE language = :language")
fun getEnglishId(language: String): LiveData<Long>
FavouriteViewModel
class FavouriteViewModel #Inject constructor(
val languageDao: LanguageDao
) : ViewModel() {
val favouriteLanguage = languageDao.getFavouriteLanguageByName().asLiveData()
// Try to access get the id in language model
val englishObject: LiveData<Long> = languageDao.getEnglishId("English")
fun onFavouriteLanguage(languageId: Int) = CoroutineScope(Dispatchers.IO).launch {
languageDao.resetFavouriteLanguage()
languageDao.setFavouriteLanguage(languageId)
}
}
FavouriteFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FavouriteBottomSheetBinding.bind(view)
val favouriteAdapter = FavouriteAdapter(this)
// Try to try to get the value from viewModel
val getEnglishId: LiveData<Long> = viewModel.englishObject
d("readEnglishId", getEnglishId.value.toString())
When you call getEnglishId.value, the database read hasn't completed and the LiveData contains the initial value null. You need to observe the LiveData in your fragment.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FavouriteBottomSheetBinding.bind(view)
val favouriteAdapter = FavouriteAdapter(this)
viewModel.englishObject.observe(viewLifecycleOwner) { id ->
// Here you can use the live data value (id)
}
}

Combining Two Flows and add them to temporary list of StateFlow(Hot Flows)

I m getting data from two End points using flows and assigning those two list to temporary list in ViewModel. For this purpose, I'm using combine function and returning result as stateFlows with stateIn operator but that's not working. Can anyone point me out where I go wrong please.
ViewModel.kt
private val _movieItem: MutableStateFlow<State<List<HomeRecyclerViewItems>>> =
MutableStateFlow(State.Loading())
val movieItems: StateFlow<State<List<HomeRecyclerViewItems>>> = _movieItem
fun getHomeItemList() {
viewModelScope.launch {
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList // This is not working as "+" Unresolve Error
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
State.loading<Nothing>()
) as State<List<HomeRecyclerViewItems>> // Unchecked cast: StateFlow<Any> to State<List<HomeRecyclerViewItems>>
}
Repository.kt
fun getMovieList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Movie>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Movie>, List<HomeRecyclerViewItems.Movie>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Movie>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Movie>> =
apiInterface.getMoviesList()
}.asFlow()
}
fun getDirectorList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Directors>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Directors>, List<HomeRecyclerViewItems.Directors>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Directors>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Directors>> =
apiInterface.getDirectorsList()
}.asFlow()
}
Network BoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Server Problem! Please try again Later. "))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal()
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Endpoints with Sealed Class
#GET("directors")
fun getDirectorsList(): Response<List<HomeRecyclerViewItems.Directors>>
#GET("movies")
fun getMoviesList(): Response<List<HomeRecyclerViewItems.Movie>>
sealed class HomeRecyclerViewItems {
class Title(
val id: Int,
val title: String
) : HomeRecyclerViewItems()
class Movie(
val id: Int,
val title: String,
val thumbnail: String,
val releaseDate: String
) : HomeRecyclerViewItems()
class Directors(
val id: Int,
val name: String,
val avator: String,
val movie_count: Int
) : HomeRecyclerViewItems()
}
Fragment.kt
#AndroidEntryPoint
#ExperimentalCoroutinesApi
class SettingsFragment : BaseBottomTabFragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<SettingViewModel>()
#Inject
lateinit var recyclerViewAdapter: RecyclerViewAdapter
#Inject
lateinit var bundle: Bundle
var finalList = mutableListOf<HomeRecyclerViewItems>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentSettingsBinding.inflate(layoutInflater,container,false)
val view = binding.root
binding.rvMovie.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
}
bundle.putString("Hello","hihg")
Toast.makeText(activity, "${bundle.getString("Hello")}", Toast.LENGTH_SHORT).show()
finalList.add(HomeRecyclerViewItems.Title(1,"hello"))
return view
}
private fun observeList() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
launch {
viewModel.movieItems.collect { state ->
when(state){
is State.Loading ->{
}
is State.Success->{
if (state.data.isNotEmpty()){
recyclerViewAdapter = RecyclerViewAdapter()
binding.rvMovie.adapter = recyclerViewAdapter
recyclerViewAdapter.submitList(finalList)
}
}
is State.Error -> {
Toast.makeText(activity, "Error", Toast.LENGTH_SHORT).show()
}
else -> Unit
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as MainActivity).binding.ivSearch.isGone = true
viewModel.getHomeItemList()
observeList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Note: I m following this tutorial simpliedCoding for api data for multirecyclerview but want to implement it with Kotlin State Flow. Any help in this regard is highly appreciated. Thanks.
Your problem is in here
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList
}
They are not returning a List<HomeRecyclerViewItems>, but a State<List<HomeRecyclerViewItems>. Maybe a better name for the variables are testsState and directorsState. After that it will be more clear why you need to unpack the values before combining the lists
_movieItem.value = combine(testsState, directorsState) { testsState, directorsState ->
val homeRecyclerViewItems = mutableListOf<HomeRecyclerViewItems>()
if (testsState is Success) homeRecyclerViewItems.add(testsState.data)
if (directorsState is Success) homeRecyclerViewItems.add(directorsState.data)
homeRecyclerViewItems
}

Input validation with MVVM and Data binding

I try to learn the MVVM Architecture by implementing a very simple app that takes three inputs from the user and stores them in a Room Database then display the data in a RecyclerView.
From the first try it seems to work well, then the app crashes if one of the inputs is left empty. Now, I want to add some input validations (for now the validations must just check for empty string), but I can't figure it out. I found many answers on stackoverflow and some libraries that validates the inputs, but I couldn't integrate those solutions in my app (most probably it is due to my poor implementation of the MVVM).
This is the code of my ViewModel:
class MetricPointViewModel(private val repo: MetricPointRepo): ViewModel(), Observable {
val points = repo.points
#Bindable
val inputDesignation = MutableLiveData<String>()
#Bindable
val inputX = MutableLiveData<String>()
#Bindable
val inputY = MutableLiveData<String>()
fun addPoint(){
val id = inputDesignation.value!!.trim()
val x = inputX.value!!.trim().toFloat()
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
private fun insert(point: MetricPoint) = viewModelScope.launch { repo.insert(point) }
fun update(point: MetricPoint) = viewModelScope.launch { repo.update(point) }
fun delete(point: MetricPoint) = viewModelScope.launch { repo.delete(point) }
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
}
and this is the fragment where everything happens:
class FragmentList : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
//Binding object
private lateinit var binding: FragmentListBinding
//Reference to the ViewModel
private lateinit var metricPointVm: MetricPointViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//Setting up the database
val metricPointDao = MetricPointDB.getInstance(container!!.context).metricCoordDao
val repo = MetricPointRepo(metricPointDao)
val factory = MetricPointViewModelFactory(repo)
metricPointVm = ViewModelProvider(this, factory).get(MetricPointViewModel::class.java)
// Inflate the layout for this fragment
binding = FragmentListBinding.inflate(inflater, container, false)
binding.apply {
lifecycleOwner = viewLifecycleOwner
myViewModel = metricPointVm
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerview()
}
private fun displayPoints(){
metricPointVm.points.observe(viewLifecycleOwner, Observer {
binding.pointsRecyclerview.adapter = MyRecyclerViewAdapter(it) { selecteItem: MetricPoint -> listItemClicked(selecteItem) }
})
}
private fun initRecyclerview(){
binding.pointsRecyclerview.layoutManager = LinearLayoutManager(context)
displayPoints()
}
private fun listItemClicked(point: MetricPoint){
Toast.makeText(context, "Point: ${point._id}", Toast.LENGTH_SHORT).show()
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment FragmentList.
*/
// TODO: Rename and change types and number of parameters
#JvmStatic
fun newInstance(param1: String, param2: String) =
FragmentList().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
I'm planning also to add a long click to the recyclerview and display a context menu in order to delete items from the database. Any help would be appreciated.
My recycler view adapter implementation:
class MyRecyclerViewAdapter(private val pointsList: List<MetricPoint>,
private val clickListener: (MetricPoint) -> Unit): RecyclerView.Adapter<MyViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding: RecyclerviewItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.recyclerview_item, parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(pointsList[position], clickListener)
}
override fun getItemCount(): Int {
return pointsList.size
}
}
class MyViewHolder(private val binding: RecyclerviewItemBinding): RecyclerView.ViewHolder(binding.root){
fun bind(point: MetricPoint, clickListener: (MetricPoint) -> Unit){
binding.idTv.text = point._id
binding.xTv.text = point.x.toString()
binding.yTv.text = point.y.toString()
binding.listItemLayout.setOnClickListener{
clickListener(point)
}
}
}
Try the following,
fun addPoint(){
val id = inputDesignation.value!!.trim()
if(inputX.value == null)
return
val x = inputX.value!!.trim().toFloat()
if(inputY.value == null)
return
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
Edit:
you can try the following as well if you wish to let the user know that the value a value is expected
ViewModel
private val _isEmpty = MutableLiveData<Boolean>()
val isEmpty : LiveData<Boolean>
get() = _isEmpty
fun addPoint(){
val id = inputDesignation.value!!.trim()
if(inputX.value == null){
_isEmpty.value = true
return
}
val x = inputX.value!!.trim().toFloat()
if(inputY.value == null){
_isEmpty.value = true
return
}
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
//since showing a error message is an event and not a state, reset it once its done
fun resetError(){
_isEmpty.value = null
}
Fragment Class
metricPointVm.isEmpty.observe(viewLifecycleOwner){ isEmpty ->
isEmpty?.apply{
if(it){
// make a Toast
metricPointVm.resetError()
}
}
}

Reload RecyclerView after data change with Room, ViewModel and LiveData

I am trying, without success, to solve a problem for days. I would like to update my recyclerView whenever the records of a particular model change in the Database (DB Room). I use ViewModel to handle the model data and the list of records are stored in LiveData.
Database
#Database(entities = arrayOf(Additive::class), version = ElementDatabase.DB_VERSION, exportSchema = false)
abstract class ElementDatabase() : RoomDatabase() {
companion object {
const val DB_NAME : String = "element_db"
const val DB_VERSION : Int = 1
fun get(appContext : Context) : ElementDatabase {
return Room.databaseBuilder(appContext, ElementDatabase::class.java, DB_NAME).build()
}
}
abstract fun additivesModels() : AdditiveDao
}
Model
#Entity
class Additive {
#PrimaryKey #ColumnInfo(name = "id")
var number : String = ""
var dangerousness : Int = 0
var description : String = ""
var names : String = ""
var notes : String = ""
var risks : String = ""
var advice : String = ""
}
Dao
#Dao
interface AdditiveDao {
#Query("SELECT * FROM Additive")
fun getAllAdditives() : LiveData<List<Additive>>
#Query("SELECT * FROM Additive WHERE id = :arg0")
fun getAdditiveById(id : String) : Additive
#Query("DELETE FROM Additive")
fun deleteAll()
#Insert(onConflict = REPLACE)
fun insert(additive: Additive)
#Update
fun update(additive: Additive)
#Delete
fun delete(additive: Additive)
}
ViewModel
class AdditiveViewModel(application: Application) : AndroidViewModel(application) {
private var elementDatabase : ElementDatabase
private val additivesModels : LiveData<List<Additive>>
init {
this.elementDatabase = ElementDatabase.get(appContext = getApplication())
this.additivesModels = this.elementDatabase.additivesModels().getAllAdditives()
}
fun getAdditivesList() : LiveData<List<Additive>> {
return this.additivesModels
}
fun deleteItem(additive : Additive) {
DeleteAsyncTask(this.elementDatabase).execute(additive)
}
private class DeleteAsyncTask internal constructor(private val db: ElementDatabase) : AsyncTask<Additive, Void, Void>() {
override fun doInBackground(vararg params: Additive): Void? {
db.additivesModels().delete(params[0])
return null
}
}
}
Fragment
class AdditivesFragment : LifecycleFragment() {
private var viewModel : AdditiveViewModel? = null
private var adapter : AdditivesAdapter? = null
companion object {
fun newInstance() : AdditivesFragment {
val f = AdditivesFragment()
val args = Bundle()
f.arguments = args
return f
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.fragment_additives, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
this.adapter = AdditivesAdapter(ArrayList<Additive>())
this.additives_list.layoutManager = GridLayoutManager(this.context, 2, GridLayoutManager.VERTICAL, false)
this.additives_list.adapter = this.adapter
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
this.viewModel?.getAdditivesList()?.observe(this, Observer<List<Additive>> { additivesList ->
if(additivesList != null) {
this.adapter?.addItems(additivesList)
}
})
super.onActivityCreated(savedInstanceState)
}
}
Now, my question is why is the observer called only once (at the start of the fragment) and then is not called back again? How can I keep the observer constantly listening to the changes in the DB (insert, update, delete) so that my recyclerView instantly can be updated? Thanks a lot for any suggestion.
This is where you made a mistake:
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
you are passing this while you are inside the fragment which is pretty disturbing for some people cause it is not a syntax error but logical. You have to pass activity!! instead, it will be like this:
this.viewModel = ViewModelProviders.of(activity!!).get(AdditiveViewModel::class.java)
UPDATE:
Pass viewLifecycleOwner while being inside fragment while observing the Data
mainViewModel.data(viewLifecycleOwner, Observer{})
If you're using fragmentKtx, you can init viewModel this way:
private val viewModel by viewModels<MainViewModel>()
If You've viewModelFactory:
private val viewModel by viewModels<MainViewModel>{
viewModelFactory
}
with this approach you don't need to call:
// you can omit this statement completely
viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
You can simply just start observing the data..

Categories

Resources