Well, I have a question, how to pass a function or its value to the second fragment? I am using the MVVM structure? I am exactly making an application in which in settingsFragment you select the csv file you want to read and send the result to the fragment with the graph and draws the graph for you. I've already done selecting the csv file but don't know how to read it and transfer the data from the file to the second fragment? Take a look at my code, if there is anything incomprehensible in the question or code, ask
SettingsFragment
class SettingsFragment : PreferenceFragmentCompat() {
private val SETTINGS_DEBUG = "PROFILE_DEBUG"
private var resolver = requireActivity().contentResolver
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val myPref: Preference? = findPreference("load_csv_file") as Preference?
myPref?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/*"
startActivity(intent)
}catch (exc: Exception) {
Log.d(SETTINGS_DEBUG, exc.message.toString())
}
true
}
fun readCSV(uri: Uri?): List<String> {
if (uri != null) {
val csvFile = resolver.openInputStream(uri)
val isr = InputStreamReader(csvFile)
return BufferedReader(isr).readLines()
}
return Collections.emptyList()
}
}
}
ChartFragment
class ChartFragment : Fragment() {
private var _binding: FragmentChartBinding? = null
private val binding get() = _binding!!
private var resolver = requireActivity().contentResolver
private val lineChartVm by viewModels<ChartViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentChartBinding.inflate(inflater, container, false)
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
val lines = readCSV(uri)
val newEntries = lines.map { line -> toEntry(line) }.groupingBy { entry -> entry.x }
.reduce { _, accumulator, element -> if (accumulator.y > element.y) accumulator else element }.values
val lineChart = binding.lineChart
val vl = LineDataSet(newEntries.toList().take(4000), "cost")
vl.setDrawValues(false)
vl.setDrawFilled(true)
vl.lineWidth = 1.5f
vl.fillColor = R.color.gray
vl.fillAlpha = R.color.red
vl.setDrawCircles(false)
lineChart.data = LineData(vl)
lineChart.notifyDataSetChanged()
lineChart.animateX(1800, Easing.EaseInExpo)
lineChart.description.isEnabled = false
lineChart.isHighlightPerDragEnabled = false
lineChart.isScaleYEnabled = false
lineChart.axisRight.isEnabled = false
}
val markerView = CustomMarker(activity?.applicationContext, R.layout.marker_view)
binding.lineChart.marker = markerView
return binding.root
}
private fun toEntry(line: String): Entry {
val split = line.split(";")
val time = split[1]
// idx 01234 012345 l:5 lub 6
// val 84504 165959
// 0, 3 - 845
val secondsStartIdx = time.length - 2
val minutesStartIdx = time.length - 4
val hoursStartIdx = (time.length - 6).coerceAtLeast(0)
val hour = time.substring(hoursStartIdx, hoursStartIdx + time.length - 4)
val minutest = time.substring(minutesStartIdx, minutesStartIdx + 2)
val seconds = time.substring(secondsStartIdx, secondsStartIdx + 2)
val newTime =
hour.toFloat() * 10000 + (minutest.toFloat() * 100 / 60).toInt() * 100 + (seconds.toFloat() * 100 / 60).toInt()
return Entry(newTime, split[2].toFloat())
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
There is not a lot of detail in the question, so I can only give a very general example and use made up class names and function arguments. This should give you an idea of the basic pattern to use.
Use a shared ViewModel in both Fragments. That means it is scoped to the Activity instead of the Fragment lifecycle. You can do this by using the activityViewModels() property delegate. In both Fragments, it would look like this:
val myViewModel: MyViewModel by activityViewModels()
In MVVM, your ViewModel class should be responsible for finding the file and reading it (or delegating to some other class to do so), so your readCSV function should be moved there. And it can parse the results into however you need them. File reading needs to be done on a background thread, and this is most easily done using coroutines started from viewModelScope. Results can be published to a LiveData.
class MyViewModel : ViewModel() {
private val mutableCsvResultLiveData = MutableLiveData<List<String>>()
val csvResultLiveData: LiveData<List<String>> get() = mutableCsvResultLiveData
fun readCSV(uri: Uri?, resolver: ContentResolver) {
uri ?: return // I would just make uri non-nullable so you don't need this
viewModelScope.launch(Dispatchers.IO) {
try {
val csvFileInputStream = resolver.openInputStream(uri)
check(csvFileInputStream != null) { "ContentResolver provider crashed" }
val isr = InputStreamReader(csvFileInputStream)
val result = BufferedReader(isr).readLines()
mutableCsvResultLiveData.postValue(result)
} catch (e: Exception) {
Log.e("readCSV", "Failed to read file", e)
return#launch
}
}
}
}
Then one fragment can initiate the file operation and parsing by calling myViewModel.readCSV(). The other fragment can observe the LiveData to react to the data when it arrives:
myViewModel.csvResultLiveData.observe(viewLifecycleOwner) { csvLines ->
// do something with csvLines, a List<String>
}
Related
I have one pretty big complicated ViewModel and I want to split it or build it with few smaller ViewModels.
Below I want to show how I make my ViewModels in general (please do not laugh, this is my first Android ViewModel). I'm not using DataBinding, just ViewBinding.
class AssignUserTagToInventoryItemViewModel() : ViewModel() {
private val UserTag = "MyApp" + this.javaClass.simpleName
init {
Log.d(UserTag, "Class init called")
loadInventoryItems()
loadRandomUserTags() // todo: replace with real implementation
}
private var allItems = ArrayList<InventoryItemDto?>()
//<editor-fold desc="FilterByName">
private val _filterByName = MutableLiveData<String>("")
val filterByName: LiveData<String> get() = _filterByName
fun setFilterByName(t : String) { _filterByName.value = t; applyFilters();}
//</editor-fold>
//<editor-fold desc="FilterByAssignedToMe">
private val _filterByAssignedToMe = MutableLiveData<Boolean>(false)
val filterByAssignedToMe: LiveData<Boolean> get() = _filterByAssignedToMe
fun setFilterByAssignedToMe(t : Boolean) { _filterByAssignedToMe.value = t; applyFilters(); }
//</editor-fold>
//<editor-fold desc="SelectedInventoryItem">
private val _selectedInventoryItem = MutableLiveData<InventoryItemDto?>(null)
fun getSelectedInventoryItem() : LiveData<InventoryItemDto?> = _selectedInventoryItem
fun setSelectedInventoryItem(itemDto: InventoryItemDto?) {
_selectedInventoryItem.value = itemDto
selectedItemOrUserTagChanged()
}
//</editor-fold>
// <editor-fold desc="FilteredItems">
val _displayedItems = MutableLiveData<ArrayList<InventoryItemDto?>>(ArrayList())
val displayedItems: LiveData<ArrayList<InventoryItemDto?>> get() = _displayedItems
// </editor-fold>
// <editor-fold desc="ItemsListError">
val _itemsListError = MutableLiveData<String>("")
val itemsListError :LiveData<String> get() = _itemsListError
fun setItemsListError(s : String) { _itemsListError.value = s }
// </editor-fold>
//<editor-fold desc="UserTag list">
val _UserTags = MutableLiveData<ArrayList<UserTag>>(ArrayList())
val UserTags : LiveData<ArrayList<UserTag>> get() = _UserTags
fun setUserTags(a : ArrayList<UserTag>) { _UserTags.value = a }
//</editor-fold>
//<editor-fold desc="SelectedUserTagItem">
private val _selectedUserTag = MutableLiveData<UserTag?>(null)
fun getSelectedUserTag() : LiveData<UserTag?> = _selectedUserTag
fun setSelectedUserTag(UserTag : UserTag?) {
_selectedUserTag.value = UserTag
selectedItemOrUserTagChanged()
}
//</editor-fold>
//<editor-fold desc="CanSubmit">
private val _canSubmit = MutableLiveData<Boolean>(false)
val canSubmit: LiveData<Boolean> get() = _canSubmit
//</editor-fold>
private fun selectedItemOrUserTagChanged() {
_canSubmit.value = true
}
private fun loadInventoryItems(){
Log.d(UserTag, "Loading inventory items...")
viewModelScope.launch {
try {
val apiResponse = ApiResponse(ApiAdapter.apiClient.findAllInventoryItems())
if (apiResponse.code == 200 && apiResponse.body != null) {
allItems = apiResponse.body
applyFilters()
Log.d(UserTag, "Loading inventory items done.")
}
else {
setItemsListError(apiResponse.code.toString())
Log.d(UserTag, "Loading inventory items error.")
}
} catch (t : Throwable) {
setItemsListError(t.message.toString())
}
}
}
private fun applyFilters(){
Log.d(UserTag, "ViewModel apply filters called. Current name filter: ${filterByName.value}")
val tempResults = ArrayList<InventoryItemDto?>()
val nameFilterLowercase = filterByName.value.toString().lowercase()
if (!filterByName.value.isNullOrEmpty()) {
for (item in allItems) {
val itemNameLowercase = item?.name?.lowercase()?:""
if (itemNameLowercase.contains(nameFilterLowercase))
tempResults.add(item)
}
_displayedItems.value = tempResults
} else {
_displayedItems.value = allItems
}
}
private fun loadRandomUserTags(){
val temp = ArrayList<UserTag>()
for (i in 1..50){
val epc = getRandomHexString(24).uppercase()
val UserTag = UserTag(epc, 0, "0")
temp.add(UserTag)
}
viewModelScope.launch {
delay(100)
_UserTags.value = temp
}
}
private fun getRandomHexString(numchars: Int): String {
val r = Random()
val sb = StringBuffer()
while (sb.length < numchars) {
sb.append(Integer.toHexString(r.nextInt()))
}
return sb.toString().substring(0, numchars)
}
}
Simply create multiple view models according to the task they are performing.
There are several problems here :
Your ViewModel name is too long
You can create an object of the getRandomHexString and this way you can use it inside any other classes or ViewModels you may need in future. It also saves space inside ViewModel.
Learn about the clean architecture and follow its practices. Here, you can create a separate view model or helper class for filtering your results. If you create another view model, you can simply retrieve results from your current view model to the activity and call filter view model inside your activity. This way you can separate code blocks according to the role they play or the function they perform.
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()
}
}
}
So I'm attempting to write an app for playing podcasts from an RSS feed, mostly to challenge myself to see if I can pull it off, but I've ran into some trouble with populating the RecyclerViewer. I've been able to successfully parse the RSS feed and store it in a MutableList, using Log statements I can verify its working(in the background thread at least), but when I try to update the adapter nothing seems to happen.
I've been using The BigNerdRanch android book as my introduction to android, and I've looked at several examples for working with RecyclerViewers, but I cannot figure out what I'm doing wrong.
I can't help but wonder if I need to use a handler to pass the data from the background thread to the main thread. I can't remember where I read to use Executors.newSingleThreadExecutor() for executing the web call. I also don't know if I should be doing the actual parsing on the background thread along with the web call, or when and where I should be calling input.close() and connect.disconnect(). Or maybe I'm just inflating the wrong thing somewhere...
All the handler/adapter examples I look at are the same as what I have, the only real difference seems to be messing around threading.
At this point I only want to see it display the list.
Main Activity(I have a mockup splashScreenActivity class that starts MainActivity using runnable() and Handler().postDelayed())
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if (currentFragment == null){
val fragment = PodcastListFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.fragment_container, fragment).commit()
}
}
I basically followed an example right out of the BigNerdRanch book, except they used a database and singleton repository to initially populate their RecyclerViewer. At this point I just want to display the list its generating before moving on to build a Database/Repository/ViewModel/etc.
class PodcastListFragment : Fragment() {
private var podcastList : MutableList<Podcast> = mutableListOf() //this is just for short term to see it work
private lateinit var podcastRecyclerView: RecyclerView
private var podcastAdapter: PodcastAdapter? = PodcastAdapter(podcastList)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.podcast_list, container, false)
podcastRecyclerView = view.findViewById(R.id.podcast_recycler_view) as RecyclerView
podcastRecyclerView.layoutManager = LinearLayoutManager(context)
podcastRecyclerView.adapter = podcastAdapter
updateUI(podcastList)
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
doInBackground() //not actually sure where this should be called
}
private fun doInBackground() {
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
var podcast = Podcast()
val url = URL(RSS)
val connect: HttpURLConnection =
url.openConnection() as HttpURLConnection
connect.readTimeout = 10000
connect.connectTimeout = 15000
connect.requestMethod = "GET"
connect.connect()
val input: InputStream = connect.inputStream
val factory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
val parser: XmlPullParser = factory.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, null)
var tagname: String?
var text = ""
var event = parser.eventType
while (event != XmlPullParser.END_DOCUMENT) {
tagname = parser.name
when (event) {
XmlPullParser.START_TAG -> if (tagname == "item") podcast = Podcast()
XmlPullParser.TEXT -> text = parser.text
XmlPullParser.END_TAG -> when(tagname){
"title" -> podcast.title = text
"itunes:author" -> podcast.author = text
"pubDate" -> podcast.date = text
"guid" -> podcast.id = parseGuid(text)
"itunes:summary" -> podcast.reference = text
"item" -> podcastList.add(podcast)
}
}
event = parser.next()
}
input.close()
connect.disconnect()
for (obj in podcastList) {Log.d(TAG, "guid: ${obj.id} :: Title: ${obj.title}")}
}
catch (e: Exception) { e.printStackTrace() }
catch (e: XmlPullParserException) { e.printStackTrace() }
catch (e: NullPointerException) { e.printStackTrace() }
}
}
// Log statements show the list is getting updated
private fun updateUI(podcasts: MutableList<Podcast>){
podcastAdapter = PodcastAdapter(podcasts)
podcastRecyclerView.adapter = podcastAdapter
}
private fun parseGuid(url: String) :String {
val equalsign = url.indexOf("=", 0, false)
return if ( equalsign != -1)
url.slice(IntRange(equalsign+1, url.length-1))
else ""
}
companion object{
fun newInstance(): PodcastListFragment{
return PodcastListFragment()
}
}
/**********************************************************************************************
*
* PodcastHolder
*
* *******************************************************************************************/
private inner class PodcastHolder(view: View) : RecyclerView.ViewHolder(view) {
private val podcastTitle: TextView = itemView.findViewById(R.id.podcast_title)
private val podcastDate: TextView = itemView.findViewById(R.id.podcast_date)
private val podcastScripture: TextView = itemView.findViewById(R.id.scripture_ref)
private val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault()) //just use a string?
fun bind(podcast: Podcast) {
podcastTitle.text = podcast.title
podcastDate.text = podcast.date
podcastScripture.text = podcast.reference
}
}
/**********************************************************************************************
*
* PodcastAdapter
*
* *******************************************************************************************/
private inner class PodcastAdapter(var podcasts: MutableList<Podcast>) : RecyclerView.Adapter<PodcastHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PodcastHolder {
val view = layoutInflater.inflate(R.layout.podcast_list_item, parent, false)
return PodcastHolder(view)
}
override fun onBindViewHolder(holder: PodcastHolder, position: Int) {
val podcast = podcasts[position]
holder.bind(podcast)
}
override fun getItemCount(): Int = podcasts.size
}
I don't get any errors, just an empty RecyclerView, and a headache trying to figure out what I did wrong.
Any guidance would be greatly appreciated.
Thanks!
EDIT
After playing around with Thread.currentThread().name I was able to figure out it was a threading problem even though I wasn't seeing an exception thrown.
Call doInBackground() inside onCreateView()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.podcast_list, container, false)
podcastRecyclerView = view.findViewById(R.id.podcast_recycler_view) as RecyclerView
podcastRecyclerView.layoutManager = LinearLayoutManager(context)
podcastRecyclerView.adapter = podcastAdapter
doInBackground(); /// <<<<<<< change here
updateUI(podcastList)
return view
}
And add notifyDataSetChanged() after the background work is over in doInBackground()
private fun doInBackground() {
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
/// .... omitted code
input.close()
connect.disconnect()
for (obj in podcastList) {Log.d(TAG, "guid: ${obj.id} :: Title: ${obj.title}")}
podcastAdapter.notifyDataSetChanged() // <<<< change here
/// ............ .... omitted code
}
I have two LiveData, aMVoice1, and aMVoice2.
I hope to check if they are equal.
I know I need to use observe to get the value of a LiveData.
so I think isEqual = (mDetailViewModel.aMVoice1.value==mDetailViewMode2.aMVoice1.value ) is wrong.
But I think there are some problems with fun observeVoice(), how can I fix it?
class FragmentDetail : Fragment() {
private lateinit var binding: LayoutDetailBinding
private val mDetailViewModel by lazy {
...
}
var isEqual=false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.lifecycleOwner = this.viewLifecycleOwner
binding.aDetailViewModel=mDetailViewModel
isEqual = (mDetailViewModel.aMVoice1.value==mDetailViewMode2.aMVoice1.value ) // I don't think it's correct.
observeVoice()
return binding.root
}
fun observeVoice() {
mDetailViewModel.aMVoice1.observe(viewLifecycleOwner){value1->
isEqual = (value1==mDetailViewModel.aMVoice2.value) // mDetailViewModel.aMVoice2.value maybe null
}
}
}
class DetailViewModel(private val mDBVoiceRepository: DBVoiceRepository, private val voiceId1:Int,private val voiceId2:Int) : ViewModel() {
val aMVoice1=mDBVoiceRepository.getVoiceById(voiceId1)
val aMVoice2=mDBVoiceRepository.getVoiceById(voiceId2)
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
fun getVoiceById(id:Int)=mDBVoiceDao.getVoiceById(id)
}
#Dao
interface DBVoiceDao{
#Query("SELECT * FROM voice_table where id=:id")
fun getVoiceById(id:Int):LiveData<MVoice>
}
data class MVoice(
#PrimaryKey (autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
var name: String = "",
var path: String = ""
)
Added Content
Is it Ok for the following code?
fun observeVoice() {
mDetailViewModel.aMVoice1.observe(viewLifecycleOwner){value1->
mDetailViewModel.aMVoice2.observe(viewLifecycleOwner){value2->
isEqual = (value1==value2)
}
}
}
According to the official documents, the best way to achieve a solution for such cases is to use MediatorLiveData as a LiveData merger. Using it, you can check the equality of values when a new value is posted on either of LiveDatas:
class DetailViewModel(...) : ViewModel() {
val areMVoicesEqual = MediatorLiveData<Boolean>().apply {
addSource(aMVoice1) { postValue(it == aMVoice2.value) }
addSource(aMVoice2) { postValue(it == aMVoice1.value) }
}
}
Then:
fun observeVoice() {
mDetailViewModel.areMVoicesEqual.observe(viewLifecycleOwner){ equality ->
// do whatever you want with `equality`
}
}
Note that Added Content snippet you mentioned is not correct. In fact, in this case, every time a value is being observed on aMVoice1, a new Observer starts to observe on aMVoice2 which is not right.
I'm using MVVM as architecture, also the repository pattern. I have a Web service, a room database also. Using coroutines block any button I click.
There's a list/detail implemented with a fragment and an activity respectively.
I can figure out what's wrong in the way I implemented the coroutines and Viewmodel.
class BuySharedViewModel(application: Application) : AndroidViewModel(application) {
private val repository: BuyRepository
var allBuys: LiveData<List<Buy>>
init {
val buyDao = KunukRoomDatabase.getDatabase(application, viewModelScope).buyDao()
val buyRemote = BuyRemote()
repository = BuyRepository.getInstance(buyDao , buyRemote)
//Use async because it return a result
viewModelScope.launch { getAllBuys() }
allBuys = buyDao.loadAllBuys()
}
private suspend fun getAllBuys() {
repository.getBuys()
}
}
Here's is the Repository, it take data from web service and add it to the room database, while ViewModel get's data from room database.
class BuyRepository (private val buyDao: BuyDao, private val buyRemote: BuyRemote) {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
companion object {
//For singleton instantiation
#Volatile private var instance: BuyRepository? = null
fun getInstance(buyDao: BuyDao, buyRemote: BuyRemote) =
instance ?: synchronized(this) {
instance ?: BuyRepository(buyDao, buyRemote)
.also { instance = it}
}
}
suspend fun getBuys(){
refresh()
}
private suspend fun refresh(){
try {
val list = scope.async { buyRemote.loadBuys() }
list.await().forEach { buy -> insert(buy) }
} catch (e: Throwable) {}
}
#WorkerThread
private fun insert(buy: Buy) {
buyDao.insertBuy(buy)
}
}
The fragment work, data are displayed, when i click on an item from that fragment(recyclerView) it work, the activity display details data. But none of the click on that activity works, like it doesn't detect the clicks. I guess it got something to do with the coroutines because when I comment out the code viewmodelScope.launch { getAllBuys()} from the BuySharedViewModel it works, because it load data from the previous call from room database, and the clicks works.
Here's the code in the detail view:
class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lateinit var buy: Buy
sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
val position = intent.getIntExtra("position", 0)
sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
buy = buys[position]
val binding: com.example.drake.kunuk.databinding.ActivityBuyDetailBinding =
DataBindingUtil.setContentView(this, com.example.drake.kunuk.R.layout.activity_buy_detail)
binding.buy = buy
val agentNumber = buy.agentNumber?:"+50937438713"
bnvContactAgent.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
com.example.drake.kunuk.R.id.action_call -> {
val callNumberUri = Uri.parse("tel:$agentNumber")
val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
startActivity(callIntent)
}
com.example.drake.kunuk.R.id.action_sms -> {
val smsNumberUri = Uri.parse("sms:$agentNumber")
val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
startActivity(smsIntent)
}
com.example.drake.kunuk.R.id.action_email -> {
val uriText = "mailto:drakecolin#gmail.com" +
"?subject=" + Uri.encode("I'm interested in $agentNumber") +
"&body=" + Uri.encode("Hello, ")
val uri = Uri.parse(uriText)
val sendIntent = Intent(Intent.ACTION_SENDTO)
sendIntent.data = uri
startActivity(Intent.createChooser(sendIntent, "Send email"))
}
}
false
}
This is the code of my fragment:
class BuyFragment : Fragment() {
companion object {
fun newInstance() = BuyFragment()
}
private lateinit var viewModel: BuySharedViewModel
private val buyList = ArrayList<Buy>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Get a new or existing ViewModel from the ViewModelProvider.
viewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
// Add an observer on the LiveData returned by loadAllBuys.
// The onChanged() method fires when the observed data changes and the activity is
// in the foreground.
viewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
// Update the cached copy of the words in the adapter.
buys?.let { (rvBuy.adapter as BuyAdapter).setBuys(it) }
progressBar.visibility = View.GONE
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.buy_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
rvBuy.layoutManager = LinearLayoutManager(context)
rvBuy.adapter = BuyAdapter(activity!!.applicationContext,
R.layout.buy_card, buyList)
progressBar.visibility = View.VISIBLE
}
}
This is the code for the BuyDao:
#Dao
interface BuyDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBuy(vararg buys: Buy)
#Update
fun updateBuy(vararg buys: Buy)
#Delete
fun deleteBuys(vararg buys: Buy)
#Query("SELECT * FROM buys")
fun loadAllBuys(): LiveData<List<Buy>>
#Query("DELETE FROM buys")
suspend fun deleteAll()
}
viewModelScope by default uses Dispatchers.Main and it is blocking your UI.
Try this:
viewmodelScope.launch(Dispatchers.IO) { getAllBuys()}
Edit:
The problem is your setting listner on BottomNavigation when your livedata is updated which is causing this weird issue.
Replace your BuyDetailActivity code with this:
class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel
private var agentNumber = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityBuyDetailBinding =
DataBindingUtil.setContentView(this, R.layout.activity_buy_detail)
binding.buy = Buy()
lateinit var buy: Buy
sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
val position = intent.getIntExtra("position", 0)
sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
buy = buys[position]
binding.buy = buy
binding.executePendingBindings()
agentNumber = buy.agentNumber
// set animation duration via code, but preferable in your layout files by using the animation_duration attribute
expandableTextView.setAnimationDuration(750L)
// set interpolators for both expanding and collapsing animations
expandableTextView.setInterpolator(OvershootInterpolator())
// or set them separately.
expandableTextView.expandInterpolator = OvershootInterpolator()
expandableTextView.collapseInterpolator = OvershootInterpolator()
// toggle the ExpandableTextView
buttonToggle.setOnClickListener {
buttonToggle.setText(if (expandableTextView.isExpanded) com.example.drake.kunuk.R.string.more else com.example.drake.kunuk.R.string.less)
expandableTextView.toggle()
}
// but, you can also do the checks yourself
buttonToggle.setOnClickListener {
if (expandableTextView.isExpanded) {
expandableTextView.collapse()
buttonToggle.setText(com.example.drake.kunuk.R.string.more)
} else {
expandableTextView.expand()
buttonToggle.setText(com.example.drake.kunuk.R.string.less)
}
}
//Open photoView activity when clicked
ivHouseDetail.setOnClickListener {
applicationContext
.startActivity(
Intent(
applicationContext,
ViewPagerActivity::class.java
)
.putExtra("imageList", buy.propertyImage)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
})
findViewById<BottomNavigationView>(R.id.bnvContactAgent)?.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.action_call -> {
Log.e("BIRJU", "Action call")
val callNumberUri = Uri.parse("tel:$agentNumber")
val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
startActivity(callIntent)
}
R.id.action_sms -> {
Log.e("BIRJU", "Action SMS")
val smsNumberUri = Uri.parse("sms:$agentNumber")
val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
startActivity(smsIntent)
}
R.id.action_email -> {
Log.e("BIRJU", "Action Email")
val uriText = "mailto:drakecolin#gmail.com" +
"?subject=" + Uri.encode("I'm interested in $agentNumber") +
"&body=" + Uri.encode("Hello, ")
val uri = Uri.parse(uriText)
val sendIntent = Intent(Intent.ACTION_SENDTO)
sendIntent.data = uri
startActivity(Intent.createChooser(sendIntent, "Send email"))
}
}
false
}
}
}