LiveData used to give callback when lifecycleOwner changed from inactive to active state, therefore we have SingleLiveEvent or Event wrapper as described in this article.
But I am not getting callback on state change if callback was given once, I have created a sample project for same
MainActivity
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this)[MainViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.tv).setOnClickListener {
startActivity(Intent(this, MainActivity2::class.java))
}
viewModel.liveData.observe(this) {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
}
}
ViewModel
class MainViewModel: ViewModel() {
val liveData = MutableLiveData<String>("Random value")
}
In this code, toast is shown when the app launches and it is not shown again if MainActivity2 is started or the app goes to background and then comes back to foreground.
Related
I wanted to check whether the phone is rooted or not. For that I wanted to check as soon as the app is clicked.
#HiltAndroidApp class ShoppingApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate() }
This is the first class which is called as soon as the app is launched. Inside this onCreate, I wanted to add check if device is rooted. If its rooted I want to show AlertDialog else navigate to next screen.
When adding composable AlertDialog, its throwing error #Composable should be called from the context of Composable.
In this case, how should I code.
You will need to create a State object in your Application class and make your composable observe that object from an Activity or Fragment.
class ShoppingApp : Application() {
val isRootedState = mutableStateOf(false)
override fun onCreate() {
super.onCreate()
if (isRootedDevice()) {
isRootedState.value = true
}
}
}
class ShoppingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val isRooted = remember { (application as ShoppingApp).isRootedState }
if (isRooted.value) {
AlertDialog(
// AlertDialog params
)
}
}
}
//...
}
So I have this in an onCreate in an activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
lifecycleScope.launch(Dispatchers.IO) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.state.collect() { state ->
println(state)
when (state) {
MyViewModel.State.First -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, FirstFragment())
}
}
MyViewModel.State.Second -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, SecondFragment())
}
}
MyViewModel.State.Init -> {}
}
}
}
}
}
and in the viewmodel I have a stateflow
class MyViewModel : ViewModel() {
enum class State {
First,
Second,
Init; }
val state = MutableStateFlow(State.Init)
fun goToFirst() {
viewModelScope.launch {
println("go to first")
state.emit(State.First)
}
}
fun goToSecond() {
viewModelScope.launch {
println("go to second")
state.emit(State.Second)
}
}
}
the app displays the list fragment and I can add and remove users its great... until the list is empty. The activity stops collecting from the stateflow and never switches to the empty. Its gets weird though. In the viewModel, as an experiment, I added
init {
while (isActive) {
delay(1000)
state.emit(State.First)
delay(1000)
state.emit(State.Second)
}
}
and it switches back forth between fragments. It just doesn't switch to the empty state fragment when I use the buttons on the screen to clear the list. I've tried using SharedFlow and I've tried using a stateflow of an enum class that had two vales list and empty. Samething. The collector in the activity doesn't fire everytime. I know about conflation. the values are different. I've tried almost every combination of Dispatchers and add in a coroutine exception handler to every launch that never catches anything. I've also tried using globalscope. Why doesn't the collector in the activity fire everytime a different value is emitted?
I was using a viewModel factory in the activity and in the fragments. So the fragment would call a function it's viewmodel to change the state, but the viewmodel the activity was listening to never sent any state change.
Hey I am working in kotlin flow in android. I noticed that my kotlin flow collectLatest is calling twice and sometimes even more. I tried this answer but it didn't work for me. I printed the log inside my collectLatest function it print the log. I am adding the code
MainActivity.kt
class MainActivity : AppCompatActivity(), CustomManager {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var time = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
viewModel.fetchData()
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.conversationMutableStateFlow.collectLatest { data ->
Log.e("time", "${time++}")
....
}
}
}
}
}
ActivityViewModel.kt
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var conversationMutableStateFlow = MutableStateFlow<List<ConversationDate>>(emptyList())
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.value = response.items
}
}
.....
}
I don't understand why this is calling two times. I am attaching logs
2022-01-17 22:02:15.369 8248-8248/com.example.fragmentexample E/time: 0
2022-01-17 22:02:15.629 8248-8248/com.example.fragmentexample E/time: 1
As you can see it call two times. But I load more data than it call more than twice. I don't understand why it is calling more than once. Can someone please guide me what I am doing wrong. If you need whole code, I am adding my project link.
You are using a MutableStateFlow which derives from StateFlow, StateFlow has initial value, you are specifying it as an emptyList:
var conversationMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
So the first time you get data in collectLatest block, it is an empty list. The second time it is a list from the response.
When you call collectLatest the conversationMutableStateFlow has only initial value, which is an empty list, that's why you are receiving it first.
You can change your StateFlow to SharedFlow, it doesn't have an initial value, so you will get only one call in collectLatest block. In ActivityViewModel class:
var conversationMutableStateFlow = MutableSharedFlow<List<String>>()
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.emit(response.items)
}
}
Or if you want to stick to StateFlow you can filter your data:
viewModel.conversationMutableStateFlow.filter { data ->
data.isNotEmpty()
}.collectLatest { data ->
// ...
}
The reason is collectLatest like backpressure. If you pass multiple items at once, flow will collect latest only, but if there are some time between emits, flow will collect each like latest
EDITED:
You really need read about MVVM architecture.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
if (supportFragmentManager.findFragmentById(R.id.fragmentView) != null)
return
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentView, ConversationFragment())
.commit()
}
}
Delele ActivityViewModel and add that logic to FragmentViewModel.
Also notice you don't need use AndroidViewModel, if you can use plain ViewModel. Use AndroidViewModel only when you need access to Application or its Context
I am developing an android app using Jetpack library:
Hilt
Navigation
ViewModel
DataBinding
Actually, I am familiar with MVP pattern.
I am trying to study MVVP pattern (Databinding and Jetpack ViewModel)
I have 2 fragments (A and B).
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
#AndroidEntryPoint
class AFragment {
private val viewModel: AViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel
with(binding) {
button.setOnClickListener {
this#AFragment.viewModel.doAction()
}
}
viewModel.result.observe(viewLifecycleOwner) { result ->
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
}
}
}
And here is AViewModel:
#HiltViewModel
class AViewModel #Inject constructor(): ViewModel() {
private val _result: MutableLiveData<Int> = MutableLiveData()
val result: LiveData<Int>
get() = _result
fun doAction() {
_result.postValue(SOME_ACTION_RESULT)
}
}
It shows BFragment correctly.
But If I touch Back Button on BFragment, it still shows BFragment.
Actually, It went to back AFragment, but it comes again to BFragment.
When I touch Back Button on BFragment,
AFragment is started again (I checked onViewCreated() is called again)
Below observe code is called again:
viewModel.result.observe(viewLifecycleOwner) { result ->
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
}
Why this code is called again?
And do I write code correctly?
What is the best practice?
Now, I found a solution.
In AFragment:
viewModel.result.observe(viewLifecycleOwner) { result ->
if (result != null) {
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
viewModel.resetResult()
}
}
and In AViewModel:
fun resetResult() {
_result.postValue(null)
}
With this code, it works fine.
Yes... But I don't like this code...
It's... so weird...
I don't know what is the best practice...
the problem is related with livedata and fragment lifecycle.
AFragment and AViewModel lives when you move to FragmentB, but view in AFragment detach and attach when you move and come back. It means onViewCreated() called every time when you touch Back button on BFragment. As a result, AFragment start to observe AViewModel which has already valid data with its _result livedata.
You should separate uidata and events in livedatas. Easiest solution is SingleEventLiveData implementation and use it.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
in viewmodel:
private val _result: MutableLiveData<Event<Int>> = MutableLiveData()
val result: LiveData<Event<Int>>
get() = _result
fun doAction() {
_result.postValue(Event(5))
}
how to observe:
viewModel.result.observe(viewLifecycleOwner) { result ->
result.getContentIfNotHandled()?.let {
findNavController().navigate(R.id.action_fragment_a_to_fragment_b)
}
}
sources:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
How to create LiveData which emits a single event and notifies only last subscribed observer?
On a button click i have to get some value from API call and then launch one screen. I have two options:
Call the observer each time when user will click on button.
Call the observer on fragment onActivityCreated() and store the value in variable and act accordingly on button click.
So which approach I should follow?
Actually it's up to you. But i always prefer to call it in Activity's onCreate() function, so activity only has 1 observer. If you call it in button click, it will give you multiple observers as much as button clicking
Here is some example :
class HomeProfileActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initObserver()
initView()
}
private fun initObserver() {
viewModel.profileWorkProccess.observe(this, {
swipeRefreshLayout.isRefreshing = it
})
viewModel.isLoadingJobs.observe(this, {
layoutProgressBarJobs.visibility = View.VISIBLE
recyclerViewJobs.visibility = View.GONE
dotsJobs.visibility = View.GONE
})
//other viewmodel observing ......
}
private fun initView() {
imageProfile.loadUrl(user.image, R.drawable.ic_user)
textName.text = identity.user?.fullName
textAddress.text = identity.user?.city
buttonGetData.setOnClickListener { viewModel.getData(this) }
}
}
If the button is placed on the Activity, and data is displayed in the Fragment, you need to store variable in Activity ViewModel and observe it in Fragment
You only need to call observe one time when fragment is created.
For example:
class MyActivity : AppCompatActivity() {
val viewModel: MyActViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myButton.setOnClickListener { view ->
viewModel.getData()
}
}
}
class MyActViewModel: ViewModel {
val data: LiveData<String> = MutableLiveData()
fun getData() {}
}
class MyFragment: Fragment {
val actViewModel: MyActViewModel by activityViewModels()
override fun onActivityCreated(...) {
....
actViewModel.data.observe(viewLifecycleOwner, Observer { data ->
...
}
}
}