Handling screen rotation without losing data with viewModel - Android - android

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)

Related

How can I observe the change in the database in Room?

I save the users I brought from the database in the room first. And their favorite status in the database is false. Then when I favorite it, I change its status. Every time I do this the adapter reloads. I used DiffUtil for this.
In addition, I expect the status of the adapter to remain the same after favorites on the Detail page, and if favorites are made on the detail page, it should be reflected on the adapter.
Because of the function I called from viewmodel in UserListFragment, all data is reloaded when fragment occurs.
Fragment
#AndroidEntryPoint
class UserListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val args: UserListFragmentArgs by navArgs()
val binding = FragmentUserListBinding.inflate(inflater, container, false)
val viewModel = ViewModelProvider(this).get(UserListViewModel::class.java)
(this.activity as AppCompatActivity).supportActionBar?.title = "Github Users"
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
viewModel.userSearch(args.term) //!!!
binding.userListRecyclerView.adapter = UserListAdapter(UserListAdapter.OnClickListener {
val action = it.login.let { login ->
UserListFragmentDirections.actionUserListFragmentToUserDetailFragment(
login!!
)
}
findNavController().navigate(action)
},viewModel)
viewModel.users.observe(viewLifecycleOwner) {
bindRecyclerView(binding.userListRecyclerView, it)
}
return binding.root
}
}
ViewModel
#HiltViewModel
class UserListViewModel #Inject constructor(
private val repository: RoomRepository,
private val firestoreRepository: FirestoreRepository,
private val githubApiService: GithubApiService,
) :
ViewModel() {
private val _users = MutableLiveData<List<UserEntity>?>()
val users: LiveData<List<UserEntity>?>
get() = _users
private val userEntities: MutableList<UserEntity> = mutableListOf()
private val mutableMap: MutableMap<String?, Any?> = mutableMapOf()
fun userSearch(term: String) {
loadFromCache(term)
viewModelScope.launch {
val getPropertiesDeferred = githubApiService.searchUser(term)
try {
val result = getPropertiesDeferred.body()
result?.users?.forEach {
userEntities.add(
UserEntity(
term = term,
login = it.login,
avatar_url = it.avatar_url,
favorite = "no"
)
)
mutableMap.put(term,userEntities)
}
updateSearchResults(userEntities, term)
firestoreRepository.saveSearchResults(mutableMap,term)
} catch (e: Exception) {
Log.e("userListErr", e.message.toString())
}
}
}
fun favorite(login: String){
viewModelScope.launch {
val list = repository.getUserFavoriteStatus(login)
if (list.isNotEmpty()){
if (list[0].favorite == "no"){
repository.addFavorite(login)
}else{
repository.removeFavorite(login)
}
loadFromCache(list[0].term.toString())
}
}
}
private fun updateSearchResults(userEntities: List<UserEntity>, term: String) {
viewModelScope.launch(Dispatchers.IO) {
val favs = repository.getFavorites(term)
repository.insertSearchResults(userEntities)
if (favs.isNotEmpty()){
favs.forEach {
favorite(it.login.toString())
}
}
loadFromCache(term)
}
}
private fun loadFromCache(term: String) {
viewModelScope.launch() {
val list = repository.getSearchResults(term)
if (list.isNotEmpty()){
_users.value = list
}else{
Log.e("boş","boş dürüm")
}
}
}
}

Android LiveData - observe complex/nested objects

I am working on a android project with MVVM structure. I want to use LiveData as recommended.
In the samples there are always just simple objecttypes e.g. String.
But I want to put an more complex/nested objecttype into LiveData.
For example an objectstructure like this:
class ClassA {
private var testVarB = ClassB()
fun getTestVarB(): ClassB {
return this.testVarB
}
fun setTestVarB(classB: ClassB) {
this.testVarB = classB
}
fun setTxt(str: String) {
this.testVarB.getTestVarC().setStr(str)
}
}
class ClassB {
private var testVarC = ClassC()
fun getTestVarC(): ClassC {
return this.testVarC
}
fun setTestVarB(classC: ClassC) {
this.testVarC = classC
}
}
class ClassC {
private var str: String = "class C"
fun getStr(): String {
return this.str
}
fun setStr(str: String) {
if (str != this.str) {
this.str = str
}
}
}
and my ViewModel looks like this:
class MyViewModel : ViewModel() {
var classAObj= ClassA()
private var _obj: MutableLiveData<ClassA> = MutableLiveData()
val myLiveData: LiveData<ClassA> = _obj
init {
_obj.value = classAObj
}
}
and the LiveDataObject is observed in the fragment:
class FirstFragment : Fragment() {
private var viewModel = MyViewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.myLiveData.observe(
requireActivity(),
Observer<ClassA>() {
// should get fired!
Log.d("TAG", "update view")
})
}
}
So if the variable str of ClassC changes the callback should get executed.
I am looking for a smart and simple solution.
I just found this similar post:
LiveData update on object field change
This example got a depth of 1. But I am looking for a solution with arbitrarily depth.
The fact that I can not find a sample of the solution for my problem makes me suspicious.
So I guess my approach is kind of wrong or bad practice anyway.
Maybe I should look for a way breaking things down and observe just simple objects.
Has anyone a solution or opinion to this?
Thanks for your help!
Here is the solution i have worked out:
I am using the PropertyAwareMutableLiveData class from here: LiveData update on object field change
class PropertyAwareMutableLiveData<T : BaseObservable> : MutableLiveData<T>() {
private val callback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
Based on this I extended the model with an iterface/abstract class.
abstract class InterfaceObservable : BaseObservable() {
open fun setNewString(s: String) {
notifyPropertyChanged(BR.str)
}
}
class ClassA : InterfaceObservable() {
private var testVarB = ClassB()
fun getTestVarB(): ClassB {
return this.testVarB
}
fun setTestVarB(classB: ClassB) {
this.testVarB = classB
}
override fun setNewString(s: String) {
super.setNewString(s)
this.testVarB.setNewString(s)
}
}
class ClassB {
private var testVarC = ClassC()
fun getTestVarC(): ClassC {
return this.testVarC
}
fun setTestVarB(classC: ClassC) {
this.testVarC = classC
}
fun setNewString(s: String) {
this.testVarC.setStr(s)
}
}
class ClassC : BaseObservable() {
#Bindable
private var str: String = "class C"
fun getStr(): String {
return this.str
}
fun setStr(str: String) {
if (str != this.str) {
this.str = str
}
}
}
In my ViewModel I use the PropertyAwareMutableLiveData class.
class MyViewModel() : ViewModel() {
var classAObj: ClassA = ClassA()
val myLiveData = PropertyAwareMutableLiveData<ClassA>()
init {
myLiveData.value = classAObj
}
}
In the Fragment I can observe the LiveData object. If ClassC.str changes the observer will get notified and can change the UI.
class MyFragment : Fragment() {
private lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.myLiveData.observe(
viewLifecycleOwner,
Observer<ClassA> {
Log.d("TAG", "change your UI here")
})
}
}
Every property which is relevant in your UI, should only be changeable over the interface given by the class InterfaceObservable.
Thats the reason why this is not a perfect solution.
But maybe it is reasonable in your case.
The issue is from the way you create your ViewModel. You can't directly instantiate it. If you use fragment-ktx artifact you can do like that :
private val model: SharedViewModel by activityViewModels()
The fragment has his own lifecycle. So you should replace requireActivity() by viewLifeCycleOwner
viewModel.myLiveData.observe(
viewLifeCycleOwner,
Observer<ClassA>() {
// should get fired!
Log.d("TAG", "update view")
})
More information here: https://developer.android.com/topic/libraries/architecture/viewmodel#sharing

Android: Why doesn't this Transformation.map run when trying to assign to a variable?

I am trying to use the Firebase API in my project but Transformations.map for the variable authenticationState in the View Model does not run. I have been following Google's tutorial here (link goes to the ViewModel of that project).
I want to be able to add the Transformations.map code to the FirebaseUserLiveData file later but I cant seem to figure out why it doesn't run.
FirebaseUserLiveData
class FirebaseUserLiveData: LiveData<FirebaseUser?>() {
private val firebaseAuth = FirebaseAuth.getInstance()
private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
value = firebaseAuth.currentUser
}
override fun onActive() {
firebaseAuth.addAuthStateListener { authStateListener }
}
override fun onInactive() {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
SearchMovieFragmentViewModel
class SearchMovieFragmentViewModel : ViewModel() {
enum class AuthenticationState {
AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
}
var authenticationState = Transformations.map(FirebaseUserLiveData()) { user ->
Log.d("TEST", "in the state function")
if (user != null) {
AuthenticationState.AUTHENTICATED
} else {
AuthenticationState.UNAUTHENTICATED
}
}
SearchMovieFragment
class SearchMovieFragment : Fragment(), MovieSearchItemViewModel {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = searchMovieFragmentViewModel
binding.signOutButton.setOnClickListener {
AuthUI.getInstance().signOut(requireContext())
}
searchMovieFragmentViewModel.authenticationState.observe(viewLifecycleOwner, Observer { state ->
when (state) {
AUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.VISIBLE
UNAUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.GONE
}
})
return binding.root
}
}
Should be .addAuthStateListener(authStateListener) instead of { authStateListener }
That is because you are not keeping the reference of FirebaseUserLiveData() once you start observing it like Transformations.map(FirebaseUserLiveData()) { user ->.
You have to have the reference of the Livedata you are mapping or transferring to another form of Livedata.
It is like a chain of observation, All LiveData in the chain should be observed or should have some kind of observer down the line, The main use-case is to transform some form of livedata to something you want, For Example:
class YourRepository{ // your repo, that connected to a network that keeps up to date some data
val IntegerResource: LiveData<Int> = SomeRetrofitInstance.fetchFromNetwork() //updating some resource from network
}
class YourViewModel{
val repository = YourRepository()
//this will start observe the repository livedata and map it to string resource
var StringResource: Livedata<String> = Transformations.map( repository.IntegerResource ) { integerValue ->
integerValue.toString()
}
My Point is you have to keep alive the LiveData you are transforming. Hope helped.

ViewModel does not retain data after fragment recreation

I have three fragment app with a bottom navigation bar and I use NavigationUI to switch.
I also have a viewmodel which creates the data (from the assets foolder) and the fragments observe the array list of live data which I use to populate a recycler view.
My problem is whenever I switch fragments, as the fragment is recreated, I do not want the data retrieval to happen every time the fragment is recreated. Hence the use of the viewmodel. But in my case, the data in the viewmodel is not retained.
I have attached the fragment and the viewmodel code.I am not sure what is wrong here.
I have tried loggging the number of entries in the aaraylist and it comes back with 0, if I do not call the routine which populates the arraylist.
SongsFragment
private const val TAG = "Songs Fragment"
class SongsFragment : Fragment(), android.widget.SearchView.OnQueryTextListener {
private val viewmodel: SongListViewModel by lazy { ViewModelProviders.of(this).get(SongListViewModel::class.java)}
private val songListAdapter = SongListAdapter(arrayListOf())
private var raga = "All"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG, "onCreateView called")
return inflater.inflate(R.layout.fragment_songs, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "onViewCreated called")
val context: FragmentActivity? = activity
(activity as AppCompatActivity).supportActionBar?.subtitle = "Songs"
val assetsPath: AssetManager? = context?.assets
val assetList = assetsPath?.list("")
if (assetList != null) {
for (item in assetList)
Log.d("SongsFragment", assetsPath.toString() + item)
}
arguments?.let {
raga = SongsFragmentArgs.fromBundle(it).raga
}
Log.d("Song Fragment", "Raga passed: " + raga)
Log.d(TAG, "Number of songs: " + viewmodel.songList.size.toString())
if (raga == "All") {
viewmodel.allSongs()
viewmodel.allSongs(assetsPath!!)
} else {
viewmodel.songsForRaga(assetsPath!!, raga)
}
Log.d(TAG, ": Songlist size: " + viewmodel.songList.size.toString())
songList_RecyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = songListAdapter
}
songSearchView.setOnQueryTextListener(this)
observeViewModel()
}
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
Log.i("Song Fragment", "Text change:" + newText.length.toString())
viewmodel.songSearchFilter(newText)
return false
}
fun observeViewModel() {
viewmodel.songs.observe(this, Observer { songs ->
songs?.let {
Log.d("Song Fragment", "ObserveViewModel")
songListAdapter.updateSongList(songs)
}
})
}
}
SongListViewModel
private const val TAG = "SongListViewModel"
class SongListViewModel : ViewModel() {
val songs = MutableLiveData<ArrayList<Song>>()
var songList = arrayListOf<Song>()
fun allSongs() {
songList = getAllSongs()
songs.value = ArrayList(songList.sortedWith(compareBy({ it.songName })))
}
fun allSongs(assetsPath: AssetManager) {
Log.d(TAG, "allSongs called")
Log.d(TAG, "Number of songs: " + songList.size.toString())
getAllSongs(assetsPath)
songs.value = ArrayList(songList.sortedWith(compareBy({ it.songName })))
}
fun songSearchFilter(text: String) {
var filteredList = arrayListOf<Song>()
filteredList.clear()
if (text.length != 0) {
for (song in songList) {
if (song.songName.toLowerCase().contains(text)) {
filteredList.add(song)
}
}
songs.value = ArrayList(filteredList.sortedWith(compareBy({ it.songName })))
} else {
songs.value = ArrayList(songList.sortedWith(compareBy({ it.songName })))
}
}
fun songsForRaga(assetsPath: AssetManager, raga: String) {
Log.d(TAG, "songsForRaga called")
var filteredList = arrayListOf<Song>()
filteredList.clear()
allSongs(assetsPath)
for (song in songList) {
if (song.raga == raga) {
filteredList.add(song)
}
}
songs.value = ArrayList(filteredList.sortedWith(compareBy({ it.songName })))
}
fun getAllSongs(assetsPath: AssetManager) {
Log.d(TAG, "getAllSongs called")
val bufferedReader = assetsPath.open("test.csv").bufferedReader()
val lineList = mutableListOf<String>()
bufferedReader.useLines { lines -> lines.forEach { lineList.add(it) } }
lineList.forEach {
val parts = it.split(",")
songList.add(Song(parts[0], parts[1], parts[2], parts[3], ""))
}
}
}
Retrieve the viewmodel by passing viewmodelprovider the fragments parent activity instead of fragment itself.
ViewModelProviders.of(activity).get(SongListViewModel::class.java)
I don't consider it as an answer, just a little explanation, your viewModel lifecycle is connected to your fragments lifecycle so it's expectedly that when you changing fragments - it destroyed and concerning your viewModel destroyed too. The solution is move creating viewmodel to activity and pasing it like Nezih adviced, but I'm not sure it is the best option.
Note that we need create the ViewModel instance in activity scope, otherwise android will create a separate instance rather than sharing the same instance and we will not get the data.
For fragment do it like this:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(SongListViewModel::class.java)
viewmodel.songs.observe(this, Observer { songs ->
songs?.let {
Log.d("Song Fragment", "ObserveViewModel")
songListAdapter.updateSongList(songs)
}
})
}

Error "Can't create ViewModelProvider for detached fragment" when i called method from Presenter

I have a fragment with updateToolbar() function. When i try to get my ViewModel, calles UserViewModel on this method i get error:
Can't create ViewModelProvider for detached fragment
When i try to get UserViewModel inside onViewCreated(), all works fine. Why it happens? I call updateToolbar() after onCreateView() and I'm not create any fragment transactions before function called.
I'm started to learn Clean Architecture, and intuitively i think the reason of error can be on it, so i add this code too. I think problem about presenter, but i can't understand where exactly.
PacksFragment:
class PacksFragment : BaseCompatFragment() {
#Inject
lateinit var presenter: PacksFragmentPresenter
private var userViewModel: UserViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
LibApp.get().injector.inject(this)
...
presenter.openNewPack(packId)
}
fun updateToolbar() {
Timber.e((userViewModel == null).toString())
Timber.e(userViewModel?.getData()?.value?.coins.toString())
}
}
PacksFragmentPresenter:
class PacksFragmentPresenter #Inject constructor(
private val packsFragment: PacksFragment,
private val getCoinsFromUserCase: GetCoinsFromUserCase
) {
fun openNewPack(packId: Int) {
if (getCoinsFromUserCase.getCoinsFromUser()){
packsFragment.updateToolbar()
}
}
}
GetCoinsFromUserCase:
class GetCoinsFromUserCase {
fun getCoinsFromUser(): Boolean {
val userViewModel = UserViewModel()
userViewModel.takeCoins(10)
return true
}
}
userViewModel:
class UserViewModel : ViewModel(), UserApi {
private val data = MutableLiveData<User>()
fun setData(user: User) {
//Logged fine
Timber.e("User setted")
data.value = user
}
fun getData(): LiveData<User> {
if (data.value == null) {
val user = User()
user.coins = 200
data.value = user
}
//Logged fine, "false"
Timber.e("User getted")
Timber.e("Is user == null? %s", (data.value == null).toString())
return data
}
override fun takeCoins(value: Int) {
//Specially commented it
// getCoins(value)
}
}
UPD:
I make some changes to prevent crush - make userViewModel nullable(update PacksFragment code on the top).
But userViewModel is always null when i call updateToolbar(). Main thing fragment not removed/deleted/invisible/..., it's active fragment with button which called updateToolbar() function.

Categories

Resources