I have a Fragment code -
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val safeArgs: PetDetailsViewArgs by navArgs()
val petId = safeArgs.petId
viewModel.getPetDetailsForId(petId).observe(viewLifecycleOwner, {
// ...
})
}
I have a ViewModel code -
private val viewState = PetDetailsViewState()
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
return if (id.isNullOrEmpty()) {
liveData {
emit(
viewState.copy(
loading = false,
error = ErrorType.PET_ID_NULL_OR_EMPTY
)
)
}
} else {
petDetailsLiveData
}
}
var petDetailsLiveData = petService.performPetAction(PetAction.GetPetDetails("2")).map {
when (it) {
// ...
}
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
As you see in my ViewModel, I am at the moment hardcoding the id in PetAction.GetPetDetails("2") which is not correct.
How do I pass the id from my view to viewModel?
You have two options, if the petId (from the Fragment) does not change, you could create / inject your ViewModel and pass the petId via Constructor.
Can your petId be null? If not you can then directly initialize your LiveData and observe it from your Fragment.
class PetViewModel(petId: String): ViewModel() {
val petDetailsLiveData = petService.performPetAction(PetAction.GetPetDetails(petId)).map {
// ...
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
Second option, as you showed in your question, if petId can change, create the LiveData within the function getPetDetailsForId(id: String?).
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
return if (id.isNullOrEmpty()) {
liveData {
emit(
viewState.copy(
loading = false,
error = ErrorType.PET_ID_NULL_OR_EMPTY
)
)
}
} else {
petService.performPetAction(PetAction.GetPetDetails("2")).map {
// ...
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
After discussion
You can consider some caching of your petId and the PetDetailsViewState to avoid duplicate api calls. Take this a a very simple example of getting the idea. There is much to improve here.
class PetViewModel : ViewModel() {
private val cachedPetDetailsViewState: PetDetailsViewState? = null
private val cachedPetId: String = ""
fun getPetDetailsForId(id: String?): LiveData<PetDetailsViewState> {
if (id == cachedPetId && cachedPetDetailsViewState != null) return MutableLiveData(cachedPetDetailsViewState)
cachedPetId == id
if (id.isNullOrEmpty() { ... }
else {
val petIdViewState = // make the API call
cachedPetDetailsViewState = petIdViewState
return MutableLiveData(petIdViewState)
}
}
}
Found a way to do with savedStateHandle -
Here is my Fragment -
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.petDetailsViewData.observe(viewLifecycleOwner, {
})
}
ViewModel -
class PetDetailsViewModel #ViewModelInject constructor(
private val petService: PetService,
#Assisted private val savedStateHandle: SavedStateHandle
) :
ViewModel() {
private val viewState = PetDetailsViewState()
var petDetailsViewData =
petService.performPetAction(PetAction.GetPetDetails(savedStateHandle.get<String>("petId")!!))
.map {
when (it) {
// ...
}
}.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)
}
I basically use safeArgs key inside viewModel and access it via savedStateHandle. This way I don't need to bother my view with accessing ids and also on configuration change, I only call my service once.
Related
I have two requests that depend on each other and I want to pass both results to the recyclerView adapter.
Unfortunately, following code does not return data_1 in collect. But when I commented out second collect with data_2 then I see result for data_1.
class MyViewModel(UUID: GUID): ViewModel() {
private val _data_1: MutableStateFlow<List<Example>> = MutableStateFlow(emptyList())
val data_1: StateFlow<List<Example>> get() = _data_1.asStateFlow()
private val _data_2: MutableStateFlow<List<Example_2>> = MutableStateFlow(emptyList())
val data_2: StateFlow<List<Example_2>> get() = _data_2.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
_repository.getData(UUID).collect { data_1 ->
_data_1.value = data_1
getSecondData(data_1)
}
}
private suspend fun getSecondData(item: List<Example>) {
val list = mutableListOf<Event>()
coroutineScope {
for (item in items) {
async(Dispatchers.IO) {
_repository.getSecondData(item).collect { it ->
list.add(it)
}
}.await()
}
_data_2.value = list
}
}
}
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
myViewModel.data_1.collect { data_1 ->
myViewModel.data_2.collect { data_2 ->
withContext(Dispatchers.Main) {
binding.myRecyclerView.adapter = MyListAdapter(data_1, data_2)
}
}
}
}
}
}
I have check with Log.d that data_1 with data_2 returns records. I have tried storing data_1 in private variable in fragment before calling second collect. I have tried joinAll option but together with launch/collect it will never work.
Why is it not working and what should I do to fix it?
As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).
My DataStore keeps returning null even though I've set a default value on the preferences manager using the elvis operator. Also, my edit function to set a preference on a key-value pair isn't being called so I'm not even sure my datastore is properly setup in general. I'm pretty sure the class is properly injected though, because I can see it as a variable while using breakpoints.
Basically val countryCode = viewModel.countrySettings.value on the ViewModel always returns null
PreferencesManager class
const val TAG = "PreferencesManager"
const val DEFAULT_COUNTRY_PREFERENCE = "us"
const val DEFAULT_CATEGORY_PREFERENCE = "general"
private val Context.dataStore by preferencesDataStore(name = PREFERENCES_NAME)
#Singleton
class PreferencesManager #Inject constructor(#ApplicationContext appContext: Context) {
private val preferencesDataStore = appContext.dataStore
//Pairs are separated but I'll create an appropriate data class later.
val countrySettings = preferencesDataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error while trying to read user preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
val country = preference[PreferenceKeys.COUNTRY] ?: DEFAULT_COUNTRY_PREFERENCE
country
}
val categorySettings = preferencesDataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error while trying to read user preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val category = preferences[PreferenceKeys.CATEGORY] ?: DEFAULT_CATEGORY_PREFERENCE
category
}
suspend fun setCountryPreference(country: String) {
preferencesDataStore.edit { preference ->
preference[PreferenceKeys.COUNTRY] = country
}
}
suspend fun setCategoryPreference(category: String) {
preferencesDataStore.edit { preference ->
preference[PreferenceKeys.CATEGORY] = category
}
}
private object PreferenceKeys {
val COUNTRY = stringPreferencesKey("country")
val CATEGORY = stringPreferencesKey("category")
}
}
ViewModel
class MainViewModel #Inject constructor(
private val repository: Repository,
private val preferencesManager: PreferencesManager
): ViewModel() {
val countrySettings = preferencesManager.countrySettings.asLiveData()
val categorySettings = preferencesManager.categorySettings.asLiveData()
/* .... */
fun setCountryPreference(country: String) {
viewModelScope.launch {
preferencesManager.setCountryPreference(country)
}
}
fun setCategoryPreference(category: String) {
viewModelScope.launch {
preferencesManager.setCategoryPreference(category)
}
}
}
Fragment
val viewModel: MainViewModel by activityViewModels()
private var _binding: FragmentSettingsCountryScreenBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSettingsCountryScreenBinding.bind(view)
//Using breakpoints I've noticed this function isn't even called on the preferences manager to set the value, which is weird
viewModel.setCountryPreference("us")
val countryCode = viewModel.countrySettings.value
binding.radiogroup.check(adaptPreferenceFromDataStore(countryCode!!))
binding.radiogroup.setOnCheckedChangeListener { _, checkedId ->
viewModel.setCountryPreference(adaptPreferenceToDataStore(checkedId))
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Yes, ianhanniballake is correct, it was a newbie mistake - I completely forgot I had to observe the livedata value and just then set the UI parameters. I was trying to set the preferences based on the value of a few switches (and vice-versa). Here's the proper function for setting it up:
fun setupSwitches() {
viewModel.countrySettings.observe(viewLifecycleOwner, { preference ->
binding.radioGroup.check(adaptPreferenceFromDataStore(preference))
})
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
viewModel.setCountryPreference(adaptPreferenceToDataStore(checkedId))
}
}
Then called setupSwitches() on onViewCreated.
I am implementing an "Add to favourites" functionality in a detail screen. If the user taps the FAB, I want to set the fab as selected and update my database. How could I use the same value that I am sending to the database to be used in my fragment (to be consistent, in case there is some issue while updating the DB)
Fragment
class BeerDetailsFragment : Fragment(R.layout.fragment_beer_details) {
private val viewModel by viewModels<BeerDetailsViewModel>()
private val args by navArgs<BeerDetailsFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToObservers()
viewModel.getBeer(args.beerId)
}
private fun subscribeToObservers() {
viewModel.beer.observe(viewLifecycleOwner, { resource ->
when(resource.status) {
Status.SUCCESS -> {
loadData(resource.data)
}
Status.ERROR -> {
showError(resource.message)
}
Status.LOADING -> {}
}
})
}
private fun loadData(beerDetails: BeerDomainModel?) {
if (beerDetails != null) {
Glide.with(requireContext())
.load(beerDetails.imageMedium)
.placeholder(R.drawable.ic_beer)
.error(R.drawable.ic_beer)
.fallback(R.drawable.ic_beer)
.into(beerDetailsImage)
beerDetailsName.text = beerDetails.name
beerDetailsDescription.text = beerDetails.description
fab.isSelected = beerDetails.isFavourite
fab.setOnClickListener {
viewModel.updateBeer(beerDetails)
// I shouldn't do it like this in case there is an issue while updating the DB
fab.isSelected = !beerDetails.isFavourite
}
}
}
...
View Model class
class BeerDetailsViewModel #ViewModelInject constructor(private val repository: BreweryRepository) : ViewModel() {
private val beerId = MutableLiveData<String>()
fun getBeer(id: String) {
beerId.value = id
}
var beer = beerId.switchMap { id ->
liveData {
emit(Resource.loading(null))
emit(repository.getBeer(id))
}
}
fun updateBeer(beer: BeerDomainModel) {
viewModelScope.launch {
repository.updateBeer(beer)
}
}
}
Repository
class BreweryRepository #Inject constructor(private val breweryApi: BreweryApi, private val beerDao: BeerDao, private val responseHandler: ResponseHandler) {
...
suspend fun getBeer(id: String): Resource<BeerDomainModel> {
return try {
withContext(IO) {
val isInDB = beerDao.isInDB(id)
if (!isInDB) {
val response = breweryApi.getBeer(id).beer.toDomainModel()
beerDao.insert(response.toBeerEntity())
responseHandler.handleSuccess(response)
} else {
val beer = beerDao.get(id).toDomainModel()
responseHandler.handleSuccess(beer)
}
}
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
suspend fun updateBeer(beer: BeerDomainModel) {
withContext(IO) {
val dbBeer = with(beer) {
copy(isFavourite = !isFavourite)
toBeerEntity()
}
beerDao.update(dbBeer)
}
}
}
I would prefer to use a unidirectional flow with the following implementation:
Not sure how is your DAO implemented, but if you are using Room you could update your get method to return a Flow instead. That way whenever your data is updated, you will get back the updated data.
Then in your VM you just get that Flow or stream of data and subscribe to the updates. Flow has a very convenient method: asLiveData() so your code will look much cleaner.
If you are not using Room, then what I'd do is either construct a Flow or some type of stream and on updates successful updates send out the new data.
I have one activity with unspecified orientation and there is one fragment attached to that activity that has different layouts for portrait and landscape mode and on that fragment, multiple API calls on a conditional basis, my problem is that when the screen rotates all data was lost and there is a lot of data on that fragment by which I don't want to save each data on saveInstance method. I tried android:configChanges="keyboardHidden|orientation|screenSize", but this didn't solve my problem. I want to handle this problem using viewModel. Please help, Thanks in advance.
Here is my code
Repository
class GetDataRepository {
val TAG = GetDataRepository::class.java.canonicalName
var job: CompletableJob = Job()
fun getData(
token: String?,
sslContext: SSLContext,
matchId: Int
): LiveData<ResponseModel> {
job = Job()
return object : LiveData<ResponseModel>() {
override fun onActive() {
super.onActive()
job.let { thejob ->
CoroutineScope(thejob).launch {
try {
val apiResponse = ApiService(sslContext).getData(
token
)
LogUtil.debugLog(TAG, "apiResponse ${apiResponse}")
withContext(Dispatchers.Main) {
value = apiResponse
}
} catch (e: Throwable) {
LogUtil.errorLog(TAG, "error: ${e.message}")
withContext(Dispatchers.Main) {
when (e) {
is HttpException -> {
value =
Gson().fromJson<ResponseModel>(
(e as HttpException).response()?.errorBody()
?.string(),
ResponseModel::class.java
)
}
else -> value = ResponseModel(error = e)
}
}
} finally {
thejob.complete()
}
}
}
}
}
}
fun cancelJob() {
job.cancel()
}
}
ViewMode:
class DataViewModel : ViewModel() {
val TAG = DataViewModel::class.java.canonicalName
var mListener: DataListener? = null
private val mGetDataRepository: GetDataRepository = GetDataRepository()
fun getData() {
LogUtil.debugLog(TAG, "getData")
if (mListener?.isInternetAvailable()!!) {
mListener?.onStartAPI()
val context = mListener?.getContext()
val token: String? = String.format(
context?.resources!!.getString(R.string.user_token),
PreferenceUtil.getUserData(context).token
)
val sslContext = mListener?.getSSlContext()
if (sslContext != null) {
val getData =
mGetDataRepository.getData(
token
)
LogUtil.debugLog(TAG, "getData ${getData}")
mListener?.onApiCall(getData)
} else {
LogUtil.debugLog(TAG, "getData Invalid certificate")
mListener?.onError("Invalid certificate")
}
} else {
LogUtil.debugLog(TAG, "getData No internet")
mListener?.onError("Please check your internet connectivity!!!")
}
LogUtil.debugLog(TAG, "Exit getData()")
}
}
Activity:
class DataActivity : AppCompatActivity() {
val TAG = DataActivity::class.java.canonicalName
lateinit var fragment: DataFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtil.debugLog(TAG, "onCreate: Enter")
var binding: ActivityDataBinding =
DataBindingUtil.setContentView(this, R.layout.activity_data)
if (savedInstanceState == null) {
fragment = DataFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.container, fragment, DataFragment.TAG)
} else {
fragment = supportFragmentManager.findFragmentByTag(DataFragment.TAG) as DataFragment
}
LogUtil.debugLog(TAG, "onCreate: Exit")
}
}
Fragment:
class DataFragment : Fragment(), DataListener {
private var mBinding: FragmentDataBinding? = null
private lateinit var mViewModel: DataViewModel
companion object {
val TAG = DataFragment::class.java.canonicalName
fun newInstance(): DataFragment {
return DataFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_data, container, false)
mViewModel = ViewModelProvider(this).get(DataViewModel::class.java)
mViewModel.mListener = this
getData()
return mBinding?.root
}
private fun getData() {
LogUtil.debugLog(TAG, "Enter getMatchScore()")
mViewModel.getData()
LogUtil.debugLog(TAG, "Exit getMatchScore()")
}
override fun <T> onApiCall(response: LiveData<T>) {
response.observe(this, Observer {
it as DataResponseModel
//
})
}
}
The lifecycle of viewModel by default is longer than your activity (in your case, screen rotation).
ViewModel will not be destroyed as soon as activity destroyed for configuration change, you can see this link.
You seem to have made a mistake elsewhere in your activity/fragment, please put your activity/fragment code here.
In your fragment you call mViewModel.getData() in your onCreateView, and every time you rotate your activity, this method call and all store data reset and fetched again!, simply you can check data of ViewModel in your fragment and if it's empty call getData(), it also seems your ViewModel reference to your view(Fragment) (you pass a listener from your fragment to your ViewModel) and it is also an anti-pattern (This article is recommended)