I have a sharedViewModel with LiveData properties.
MainActivity:
private val logViewModel by viewModels<LogViewModel>()
fun startLocationUpdates() {
val locationItem = LocationItem(...)
logViewModel.addLogEntry(locationItem)
}
override fun onCreate(savedInstanceState: Bundle?) {
val observer = object : FileObserver(applicationContext.filesDir.path + "/" + jsonFileName) {
override fun onEvent(event: Int, file: String?) {
if (event == FileObserver.MODIFY) {
loadLogViewModel()
}
}
}
observer.startWatching()
}
fun loadLogViewModel() {
val json = getData(this)
val myType = object : TypeToken<ArrayList<LocationItem>>() {}.type
if (json != null) {
val listItems: ArrayList<LocationItem> = gson.fromJson(json, myType)
for (item in listItems) {
logViewModel.addLogEntry(item)
}
}
}
LogFragment:
private val logViewModel: LogViewModel by activityViewModels()
logViewModel.locationListItems.observe(requireActivity()) {
locationItemAdapter.updateLocations()
}
LogViewModel:
class LogViewModel : ViewModel() {
var locationListItems: MutableLiveData<LocationItem> = MutableLiveData<LocationItem>()
fun addLogEntry(locationItem: LocationItem) {
locationListItems.postValue(locationItem)
}
}
When I execute startLocationUpdates() it all works as expected. The observer gets notifications.
But when the logfile (json) is changed through the WorkManager (potentially within a different process/thread) and the FileObserver kicks in to load the JSON file and adds them to the LogViewModel, the Live data is not firing anything.
Why does it make a difference if the fileObserver is doing it, or if startLocationUpdates() would be doing it? I can only assume this is a threading issue. But I thought LivaData deals with that?
Related
my data is fetched only when it is created...im using viewmodel...when press back button it doesnt update the previous data..onresume is not working in this...
i refered this but none of those helped--> Reacting to activity lifecycle in ViewModel
i need help
thanks in advance
activity:--
class MyAccount : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
var mActionBarToolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable);
setSupportActionBar(mActionBarToolbar);
setEnabledTitle()
val resetbutton=findViewById<Button>(R.id.resetpwd)
resetbutton.setOnClickListener {
val i=Intent(applicationContext,
ResetPasswordActivity::class.java)
startActivity(i)
}
val editbutton=findViewById<Button>(R.id.editdetail)
editbutton.setOnClickListener {
val i=Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
hello()
}
override fun onResume() {
super.onResume()
hello()
}
fun hello(){
val first_name = findViewById<TextView>(R.id.firstname)
val last_name = findViewById<TextView>(R.id.lastname)
val emailuser = findViewById<TextView>(R.id.emailuser)
val phone_no = findViewById<TextView>(R.id.phone_no)
val birthday = findViewById<TextView>(R.id.birthday)
val image=findViewById<ImageView>(R.id.imageprofile)
val model = ViewModelProvider(this)[MyAccountViewModel::class.java]
model.viewmodel?.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext).load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}}
viewmodel:--
class MyAccountViewModel(context: Application) :AndroidViewModel(context),LifecycleObserver{
private var MyAccountViewModels: MutableLiveData<My_account_base_response>? = null
val viewmodel: MutableLiveData<My_account_base_response>?
get() {
if (MyAccountViewModels == null) {
MyAccountViewModels = MutableLiveData<My_account_base_response>()
loadviewmodel()
}
return MyAccountViewModels
}
private fun loadviewmodel(){
val token :String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
MyAccountViewModels!!.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}}
There are bunch of things wrong here, so let me provide you refactored code and explanation as much as I would be able to..
Activity:
class MyAccount : BaseClassActivity() {
private val mActionBarToolbar by lazy { findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable) }
private val resetbutton by lazy { findViewById<Button>(R.id.resetpwd) }
private val editbutton by lazy { findViewById<Button>(R.id.editdetail) }
private val first_name by lazy { findViewById<TextView>(R.id.firstname) }
private val last_name by lazy { findViewById<TextView>(R.id.lastname) }
private val emailuser by lazy { findViewById<TextView>(R.id.emailuser) }
private val phone_no by lazy { findViewById<TextView>(R.id.phone_no) }
private val birthday by lazy { findViewById<TextView>(R.id.birthday) }
private val image by lazy { findViewById<ImageView>(R.id.imageprofile) }
lateinit var model: MyAccountViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
setSupportActionBar(mActionBarToolbar)
setEnabledTitle()
model = ViewModelProvider(this)[MyAccountViewModel::class.java]
resetbutton.setOnClickListener {
val i = Intent(applicationContext, ResetPasswordActivity::class.java)
startActivity(i)
}
editbutton.setOnClickListener {
val i = Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
model.accountResponseData.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext)
.load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onResume() {
super.onResume()
model.loadAccountData()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
Few notes on your activity class:
You don't need to findViewById everytime, just do it once during onCreate or do it lazily. (FYI consider using kotlin synthetics or view binding or data binding)
Initialize your viewModel during onCreate method only. (That's the best way to do it)
Also observer your LiveData from ViewModel once, it should be also from the onCreate as it's the entry point to the activity and apart from config changes this method called only once. So, it's safe to observe it over there rather than during onResume which will be called multiple times during activity lifecycle. (The main issue your code wasn't working, so as a fix you only call your API method from ViewModel during resume)
ViewModel:
class MyAccountViewModel(context: Application) : AndroidViewModel(context) {
private val _accountResponseData = MutableLiveData<My_account_base_response?>()
val accountResponseData: MutableLiveData<My_account_base_response?>
get() = _accountResponseData
init {
loadAccountData()
}
fun loadAccountData() {
val token: String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
_accountResponseData.value = null
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
_accountResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
Don't make initial API call along with LiveData creation, it's okay to do in most of cases but if you're updating LiveData on response of that call then it's good to make it separately like during init block.
It's good practice not to allow Ui (Activity/Fragments) to modify LiveDatas of ViewModel directly. So, that's good sign you're following such pattern by having private MutableLiveData exposed as public LiveData, but do it correctly as suggested.
Side note: Your view model doesn't need to be LifecycleObserver. LifecycleObserver is used for some custom class/component which needs to be managed by their self by silently observing/depending on activity lifecycle independently. That's not the use case of ViewModel.
The only thing that I found why your code wasn't working correctly is because you were creating & observing ViewModel & LiveData over & over again as new objects from onResume method where you called hello() method.
Let me know if something don't make sense or missing.
I have the following ViewModel class -
class VerifyOtpViewModel : ViewModel() {
private var existingUserProfileData: MutableLiveData<TwoVerteUsers.TwoVerteUser>? = null
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(authorization, GetUserProfilesBodyModel(listOf(user)), object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData?.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
fun getExistingUserProfileData(): LiveData<TwoVerteUsers.TwoVerteUser>? {
if (existingUserProfileData == null) return null
return existingUserProfileData as LiveData<TwoVerteUsers.TwoVerteUser>
}
}
and the following observer -
private fun initViewModel() {
verifyOtpViewModel = ViewModelProvider(this).get(VerifyOtpViewModel::class.java)
verifyOtpViewModel.getExistingUserProfileData()?.observe(this, Observer {
if (it != null)
Log.d("existingProfile", it.username)
})
}
For some reason the observe is never triggered even after the MutableLiveData object is being given a value
Tried to search for a solution here at stackoverflow but nothing helped
what am I missing?
refactor your code to this, and you should be good to go:
class VerifyOtpViewModel : ViewModel() {
private val _existingUserProfileData = MutableLiveData<TwoVerteUsers.TwoVerteUser>()
val existingUserProfileData: LiveData<TwoVerteUsers.TwoVerteUser>
get() = _existingUserProfileData
fun checkInfoForAuthenticatedUser(authorization: String, user: String) {
ProfileNetworking.getUsersProfiles(
authorization,
GetUserProfilesBodyModel(listOf(user)),
object : ProfileNetworking.OnGetUserProfilesListener {
override fun onSuccess(model: TwoVerteUsers) {
existingUserProfileData.value = model[0]
}
override fun onError(reason: String) {
Log.d("existingProfile", reason)
}
})
}
}
And observing:
verifyOtpViewModel.existingUserProfileData.observe(this, Observer {
.....
})
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.
I have a LiveData object which i'm observing within a fragment within an activity. The activity listens to broadcasts from the system via 2 broadcast receivers. The data is queried from room according to the current date. So the receivers detect if the date was changed and if so, go and refresh the query.
Problem is after i change the date in the settings and come back to the app, the observer isn't triggered. If i go to another fragment and return the data will change, but not from the
Now for the code:
Activity:
private lateinit var timeReceiver: MyTimeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
timeReceiver = MyTimeReceiver()
registerReceiver(timeReceiver, filter)
registerReceiver(refreshReceiver, IntentFilter("refresh_data"))
mainFragment = MainFragment.newInstance()
}
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "refresh_data") {
Timber.e("time changed, need to refresh data")
mainFragment.refreshData()
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeReceiver)
unregisterReceiver(refreshReceiver)
}
Fragment:
private var counter: Int = 0
private lateinit var viewModel: MainFragmentViewModel
private lateinit var date: String
companion object {
fun newInstance(): MainFragment {
return MainFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
plus_btn.setOnClickListener { addCup() }
minus_btn.setOnClickListener { decreaseCup() }
viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java)
viewModel.dataList.observe(viewLifecycleOwner, Observer {
it.let { it1 ->
counter = it1.size
cup_counter.text = "$counter"
setGoalVisibility()
}
})
}
override fun onResume() {
super.onResume()
viewModel.checkCups(setDate())
}
private fun decreaseCup() {
viewModel.deleteCup(counter, date)
}
private fun addCup() {
Timber.e(date)
viewModel.insertCup(
WaterCupEntity(
System.currentTimeMillis(),
++counter,
date
)
)
}
fun refreshData() {
viewModel.checkCups(setDate())
}
private fun setDate(): String {
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
date = dateFormat.format(Calendar.getInstance().time)
return date
}
ViewModel:
class MainFragmentViewModel(application: Application) : AndroidViewModel(application) {
private var dao: WaterCupDao
var dataList: LiveData<List<WaterCupEntity>>
private var job = Job()
private val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
var date: String
init {
dao = MyCupsDb.getInstance(application, scope).getDao()
date = dateFormat.format(Calendar.getInstance().time)
Timber.e("getting all cups for date: %s", date)
dataList = dao.getAllCupsWithSameDate(date)
}
fun checkCups(date :String) {
dataList = dao.getAllCupsWithSameDate(date)
}
fun insertCup(cup: WaterCupEntity) = runBlocking {
scope.launch(Dispatchers.IO) {
dao.insert(cup)
}
}
fun deleteCup(number: Int, date: String) =
scope.launch(Dispatchers.IO) {
dao.deleteCupForDate(number, date)
}
fun deleteToday(date :String) {
scope.launch (Dispatchers.IO){
dao.deleteAllCupsForDate(date)
}
}
fun deleteAllCups() {
scope.launch (Dispatchers.IO){
dao.deleteAllCups()
}
}
}
Room db :
#Database(entities = [WaterCupEntity::class], version = 1)
abstract class MyCupsDb : RoomDatabase() {
abstract fun getDao(): WaterCupDao
companion object {
#Volatile
private var instance: MyCupsDb? = null
fun getInstance(context: Context, scope:CoroutineScope): MyCupsDb {
return instance ?: synchronized(this) {
instance ?: buildDb(context,scope).also { instance = it }
}
}
private fun buildDb(context: Context,scope:CoroutineScope): MyCupsDb {
return Room.databaseBuilder(context, MyCupsDb::class.java, "myDb")
.addCallback(WordDatabaseCallback(scope))
.build()
}
}
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onOpen(db)
instance?.let { database ->
scope.launch(Dispatchers.IO) {
populateDatabase(database.getDao())
}
}
}
fun populateDatabase(dao: WaterCupDao) {
var word = WaterCupEntity(System.currentTimeMillis(),1,"11.01.2019")
dao.insert(word)
word = WaterCupEntity(System.currentTimeMillis(),2,"11.01.2019")
dao.insert(word)
}
}
}
MyTimeReceiver:
class MyTimeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action){
Intent.ACTION_TIMEZONE_CHANGED,
Intent.ACTION_DATE_CHANGED,
Intent.ACTION_TIME_CHANGED,
Intent.ACTION_TIME_TICK -> context.sendBroadcast(Intent("refresh_data"))
}
}
}
So mainly the use case was to trigger the LiveData query after some event - in this case an intent from a BroadcastReceiver - so it is refreshed.
I wanted to put new params for the query and then make the query work.
What i didn't realize is this is basically against the design of LiveData. LiveData is data that you subscribe to, a client that subscribes to that data doesn't affect it, it merely listens to its changes.
So the problem here is with design.
To get a new query you usually need either a new data (so your observe will trigger) or resubscribe to the LiveData - the harder and more complicated approach since then you need to manage the subscriptions so you don't leak.
I chose to get a new data via using an insert once i get an intent. If anyone so wishes i can post the fixed code for this.
I am teaching myself Kotlin and android dev. So, i'm sure most of my issue is lack of knowledge, but i've been hung up on this part for day or two. I think my issue is partly my JSON query, and mostly my rookieness.
In my for loop below, I'm getting the following error from the IDE "For-Loop range must have an 'iterator()' method". This is in regards to 'cycloneList' in: for(stormInfo in cycloneList)
I've linked my "dummy" JSON data I'm using can be found here: https://api.myjson.com/bins/19uurt to save a bit of space here in the question.
Problem code
`var cycloneList = response?.body()?.currenthurricane?.stormInfo?.get(0)
if (cycloneList != null) {
for (stormInfo in cycloneList) { <<--Problem
val newCyclone = "Name: ${cycloneList.stormName}"
cycloneStrings.add(newCyclone)
}
}`
FULL CODE
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//creates a new CycloneRetriever object from CycloneHelp.kt
var retriever = CycloneRetriever()
val callback = object : Callback<Cyclones> {
override fun onResponse(call: Call<Cyclones>?, response: Response<Cyclones>?) {
println("Got a response!")
var cycloneList = response?.body()?.currenthurricane?.stormInfo?.get(0)
var cycloneStrings = mutableListOf<String>()
if (cycloneList != null) {
for (stormInfo in cycloneList) { //TODO Figure this out!!
val newCyclone = "Name: ${cycloneList.stormName}"
cycloneStrings.add(newCyclone)
}
}
var layoutMovieListView = movieListView
var movieListAdapter = ArrayAdapter(this#MainActivity, android.R.layout.simple_list_item_1,
cycloneStrings)
layoutMovieListView.adapter = movieListAdapter
}
override fun onFailure(call: Call<Cyclones>?, t: Throwable?) {
println("The thing, it failed!")
}
}
retriever.getCyclones(callback)
}
I'm using Retrofit to access/handle JSON data
Retrofit JSON code
interface WeatherWunderGroundAPI {
#GET("bins/19uurt")
fun getCyclones() : Call<Cyclones>
}
class Cyclones(val currenthurricane: CurrentHurricane)
class CurrentHurricane(val stormInfo: List<StormInfo>)
class StormInfo(val stormName: String)
class CycloneRetriever {
val service : WeatherWunderGroundAPI
init {
val = retrofitCyclone
Retrofit.Builder().baseUrl("https://api.myjson.com/")
.addConverterFactory(GsonConverterFactory.create()).build()
service = retrofitCyclone.create(WeatherWunderGroundAPI::class.java)
}
fun getCyclones(callback: Callback<Cyclones>) {
val call = service.getCyclones()
call.enqueue(callback)
}
}
Your stormInfo is a List which provides the mandatory iterator() and this is what you want to iterate over:
var cycloneList = response?.body()?.currenthurricane?.stormInfo ?: emptyList()
for (stormInfo in cycloneList) { }
Hope it helps...