I am using lifecycleScope.launch in my activity's onCreate to collect a flow but I also am trying to attach a fragment inside the scope but I just get a black screen when trying to do this like it never gets attached.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
If I dont use lifecycleScope and implement CoroutineScope in my activity with that coroutine scope the fragment attaches fine
class MainActivity : AppCompatActivity(), CoroutineScope{
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
}
launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
}
I dont understand why, both appear to be using the same context with the Dispatcher as Main.
Can someone provide insight here?
Separate the code that collects data from flow to another coroutine scope
your problem will be solved:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
_viewModel.listenForScheduleChanges().collect {
}
}
}
lifecycleScope.launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
the issue is that when you collect a flow in this way it blocks the whole scope and other codes in the scope are not running.
Related
Currently I'm facing a problem getting data from a child fragment to it's parent fragment, I saw that the best option is using setFragmentResult but because setFragmentResultListener needs to be in STARTED state at parentFragment(doen't happen because it is stoped when replaced by another fragment) I see that the only option is to use popBackStack() and then the listener gets triggered. The thing is that I don't wanna use popBackStack()
Can anyone help me?
PS: No, I don't want to use viewModel in this case to keep data.
Listener:
class ResultListenerFragment : Fragment() {
val viewModel : SomeViewModel by viewModels()
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
//verify values
viewModel.repeat()
}
//in case of an error
viewModel.getError().observe(viewLifecycleOwner,{
requireActivity()
.supportFragmentManager
.beginTransaction()
.replace(R.id.framelayout, ErrorFragment.newInstance(it).commit()
}
}
}
Triggerer:
class ErrorFragment: Fragment(R.layout.fragment_error) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.some_tv).text = arguments.getString(keyError)
view.findViewById(R.id.result_button).setOnClickListener {
val result = "retry"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
companion object {
private const val keyError = "errorKey"
fun newInstance(error:String): ErrorFragment{
val args = Bundle().apply {
putString(keyError, error)
}
}
return ErrorFragment().apply{arguments = args}
}
}
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 ->
...
}
}
}
I looked for many articles and tried to understand how Live Data is observe changes when MVVM architecture is used.
I have a Fragment A, ViewModel and Repository class.
ViewModel is initiated in onCreateView() method of the fragment.
Api call is initiated just after that in onCreateView() method of fragment.
Data from the Server is observed in onViewCreated method of the fragment.
For the first, it is running perfectly fine. But When I update the user name from another Fragment B and come back to Fragment A.
Api is called again in onResume() method of Fragment A to update UI. But here my Live Data is not observed again and UI is not updated
I didn't understand what I am doing wrong? Why observer is not triggering second time?
Below is the code
class FragmentA : Fragment(){
private lateinit var dealerHomeViewModel: DealerHomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home_dealers, container, false)
val dealerHomeFactory = DealerHomeFactory(token!!)
dealerHomeViewModel = ViewModelProvider(this,dealerHomeFactory).get(DealerHomeViewModel::class.java)
dealerHomeViewModel.getDealerHomeData()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
}
override fun onResume() {
super.onResume()
dealerHomeViewModel.getDealerHomeData()
}
}
//=========================== VIEW MODEL ===================================//
class DealerHomeViewModel(val token:String) : ViewModel() {
var dealerInfoLiveData:LiveData<DealerInfo>
init {
dealerInfoLiveData = MutableLiveData()
}
fun getDealerHomeData(){
dealerInfoLiveData = DealerHomeRepo().getDealerHomePageInfo(token)
}
}
//======================== REPOSITORY ================================//
class DealerHomeRepo {
fun getDealerHomePageInfo(token:String):LiveData<DealerInfo>{
val responseLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
responseLiveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
return responseLiveData
}
}
I think your problem is that you are generating a NEW mutableLiveData each time you use your getDealerHomePageInfo(token:String method.
First time you call getDealerHomePageInfo(token:String) you generate a MutableLiveData and after on onViewCreated you observe it, it has a value.
In onResume, you call again getDealerHomePageInfo(token:String) that generates a NEW MutableLiveData so your observer is pointing to the OLD one.
What would solve your problem is to pass the reference of your viewModel to your repository so it updates the MutableLiveData with each new value, not generate a new one each time.
Edited Answer:
I would do something like this for ViewModel:
class DealerHomeViewModel(val token:String) : ViewModel() {
private val _dealerInfoLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val dealerInfoLiveData:LiveData = _dealerInfoLiveData
fun getDealerHomeData(){
DealerHomeRepo().getDealerHomePageInfo(token, _dealerInfoLiveData)
}
}
And this for the DealerHomeRemo
class DealerHomeRepo{
fun getDealerHomePageInfo(token:String, liveData: MutableLiveData<DealerInfo>){
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
liveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
}
For Observers, use the LiveData as before:
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
Example, If I replaced 'fragmentA' with 'fragmentB', the 'viewModelA' of fragmentA is still live. why ?
onCreate() of Fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider.NewInstanceFactory().create(InvoicesViewModel::class.java)
}
ViewModel
class InvoicesViewModel : ViewModel() {
init {
getInvoices()
}
private fun getInvoices() {
viewModelScope.launch {
val response = safeApiCall() {
// Call API here
}
while (true) {
delay(1000)
println("Still printing although the fragment of this viewModel destroied")
}
if (response is ResultWrapper.Success) {
// Do work here
}
}
}
}
This method used to replace fragment
fun replaceFragment(activity: Context, fragment: Fragment, TAG: String) {
val myContext = activity as AppCompatActivity
val transaction = myContext.supportFragmentManager.beginTransaction()
transaction.replace(R.id.content_frame, fragment, TAG)
transaction.commitNow()
}
You will note the while loop inside the Coroutine still work although after replace fragment to another fragment.
this is about your implementation of ViewModelProvider.
use this way for creating your viewModel.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(InvoicesViewModel::class.java)
}
in this way you give your fragment as live scope of view model.
Check, if you have created the ViewModel in Activity passing the context of activity or fragment.
Guess I'm missing something obvious here but... I'm storing data in uiModel in the DiaryViewModel class, and since I use architecture components I'm expecting the data to be retained through screen rotation - but it doesn't. I'm blind to why.
Here's a stripped down fragment
class DiaryFragment: Fragment() {
private lateinit var viewModel: DiaryViewModel
override onCreateView(...) {
viewModel = ViewModelProviders.of(this).get(DiaryViewModel::class.java)
viewModel.getModel().observe(this, Observer<DiaryUIModel> { uiModel ->
render(uiModel)
})
}
}
And the corresponding view model.
class DiaryViewModel: ViewModel() {
private var uiModel: MutableLiveData<DiaryUIModel>? = null
fun getModel(): LiveData<DiaryUIModel> {
if (uiModel == null) {
uiModel = MutableLiveData<DiaryUIModel>()
uiModel?.value = DiaryUIModel()
}
return uiModel as MutableLiveData<DiaryUIModel>
}
}
Can any one see what's missing in this simple example? Right now, uiModel is set to null when rotating the screen.
The issue was with how the activity was handling the fragment creation. MainActivity was always creating a new fragment per rotation, as in
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager
.beginTransaction()
.replace(overlay.id, DiaryFragment.newInstance())
.commit()
}
But of course, it works much better when checking if we have a saved instance, as in
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(overlay.id, DiaryFragment.newInstance())
.commit()
}
}