Android navigation components performance issues - android

So I recently migrated to navigation components (2.2.0-alpha01). As I was working on a high end device I didn't really noticed any slowdowns, but as soon as I finished, testers started reporting slugish app navigation.
In my navigation code I use calls like findNavController().navigate(CustomFragmentDirections.actionEtc()) or findNavController().popBackStack(fragmentId, false)
I also use safeargs with navigation.
In my navigation xml I have actions that heavily rely on popUpTo and app:launchSingleTop="true"
To investigate I made very basic profiler in my BaseFragment class:
private var lastTimestamp = System.currentTimeMillis()
protected fun getEllapsedTime(): String {
val currTime = System.currentTimeMillis()
val elapsedTime = currTime - lastTimestamp
lastTimestamp = currTime
return "${elapsedTime}ms"
}
override fun onAttach(context: Context) {
Timber.d("onAttach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onAttach(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate(${javaClass.simpleName})(${getEllapsedTime()})")
super.onCreate(savedInstanceState)
savedInstanceState?.let { restoreState(it) }
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Timber.d("onCreateView(${javaClass.simpleName})(${getEllapsedTime()})")
return inflater.inflate(layoutRes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("onViewCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onViewCreated(view, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
Timber.d("onActivityCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
Timber.d("onStart(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStart()
}
override fun onResume() {
Timber.d("onResume(${javaClass.simpleName})(${getEllapsedTime()})")
super.onResume()
requireActivity().window?.decorView?.post {
firstFrameRendered()
}
}
private fun firstFrameRendered() {
Timber.d("onFrameRendered(${javaClass.simpleName})(${getEllapsedTime()})")
}
override fun onPause() {
Timber.d("onPause(${javaClass.simpleName})(${getEllapsedTime()})")
super.onPause()
}
override fun onStop() {
Timber.d("onStop(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStop()
}
override fun onDestroyView() {
Timber.d("onDestroyView(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroyView()
}
override fun onDestroy() {
Timber.d("onDestroy(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroy()
}
override fun onDetach() {
Timber.d("onDetach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDetach()
}
I tried profiling using android studio profiler, but didn't really noticed anything out of the ordinary. I also tried window.addOnFrameMetricsAvailableListener but it pretty much gave me same results as my profiler code. The main method of importance is onFrameRendered. Basic Idea is to let layout inflate and render and immediately after screen is rendered count how many milliseconds passed since onResume was called.
I tried different devices and timings were not very consistent, but after measuring same transitions many times I noticed some tendency, that all my layouts now take almost twice as long to load when compared to previous app navigation which was using simple supportFragmentManager transactions.
I tried isolating navigation from one fragment to the other and I would always get this poor performance.
At the moment I know it has something to do with the way navigation switches fragments, because if I mock NavController with my custom code that just directly uses FragmentManager I get the same good performance as the old code. Will update the question if I'll find the exact problem.
Meanwhile, does anyone have any ideas what might be wrong?

Related

SharedPreference won't apply change (in a fragment)

In my Activity there is a ViewPager2 that loads a fragment which shows texts and another fragment which intended to have options such as the size of texts which shows on the other fragment. To make it not have to set the option every time I chose to use SharedPreferences, but it won't take effects. Here is the code:
class Options : Fragment() {
lateinit var binding: OptionsLayoutBinding
private lateinit var mPreferences: SharedPreferences
val preferencesEditor: SharedPreferences.Editor get() = mPreferences.edit()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mPreferences = this.requireActivity().getSharedPreferences("pref", Context.MODE_PRIVATE)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = OptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onResume() {
super.onResume()
binding.radioGroupTextSize.setOnCheckedChangeListener { group, checkedId ->
preferencesEditor.run{
when (checkedId) {
R.id.textSize_RB1 -> putInt("TXTSZ", 12)
R.id.textSize_RB2 -> putInt("TXTSZ", 14)
R.id.textSize_RB3 -> putInt("TXTSZ", 16)
R.id.textSize_RB4 -> putInt("TXTSZ", 18)
R.id.textSize_RB5 -> putInt("TXTSZ", 20)
}
}
}
override fun onPause() {
super.onPause()
preferencesEditor.apply()
Log.d("PREF", "TEXT SIZE SET TO ${mPreferences.getInt("TXTSZ", 12)}. ")
}
}
.. The Log.d() is there to make sure where the problem happens and the log only says it's 12. It seems SharedPreferences is not saving the value, tt also doesn't take any effects to the target fragment which is meant to display texts in changed size. I wondered if apply() is placed wrong so I tried putting it after every radio button behaviours, which didn't improve the situation at all.
There are many other values neeed to be saved, but working out this one means they would work too, so I simplified the code here.
Thanks for help in advance!
Solved the problem; it creates xml but didn't write anyting on it. I still don't know why it worked that way, perhaps(most likely) it's a spaghetti code, so I just put simple codes that confirms working on an activity. It won't in a fragment, and made a simple change to fix it.
lateinit var binding: OptionsLayoutBinding
var preferencesEditor: SharedPreferences? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferencesEditor = activity?.getSharedPreferences("pref", Context.MODE_PRIVATE)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = OptionsLayoutBinding.inflate(inflater, container, false)
return binding.root
}
and putting .apply() after every putInt(). Made a custom function to not repeat it.

refresh the fragment when getting back from an activity or pressing back

I was trying to refresh the fragment when pressing back from an activity, I tried using onResume() and onStop() and it worked but... another issue comes. using onResume() and onStop() inside the fragment is making the fragment refresh too many times that the app is crashing and I really don't know what I am doing wrong, if you can please help me with this issue
my onResume() function
override fun onResume() {
super.onResume()
//shoudRefreshOnResume is a global var
if (shouldRefreshOnResume) {
val ft: FragmentTransaction = parentFragmentManager.beginTransaction()
ft.detach(this).attach(this).commit()
}
}
my onStop() function
override fun onStop() {
super.onStop()
shouldRefreshOnResume = true
}
my onCreateView() function
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_home, container, false)
val foodButton = root.findViewById<Button>(R.id.mainFoodButton)
val recentlyViewed = root.findViewById<LinearLayout>(R.id.recently_viewedView)
foodButton.setOnClickListener {
val intent = Intent(activity, CategoriesActivity::class.java)
startActivity(intent)
}
//createRecentlyViewedButton() is a function
createRecentlyViewedButton(recentlyViewed)
return root
}
I fixed the issue by replacing onStop() function to onPause() since the activity is not getting destroyed and it no longer loop the createRecentlyViewedButton() function hope this help somebody
here are the changes I made though
override fun onPause() {
super.onPause()
shouldRefreshOnResume = true
}
and
override fun onResume() {
super.onResume()
//shoudRefreshOnResume is a global var
if (shouldRefreshOnResume) {
val recentlyViewed = activity?.findViewById<LinearLayout>(R.id.recently_viewedView)
createRecentlyViewedButton(recentlyViewed!!)
}
}

Cannot populate spinner with data from database?

I'm trying to populate a spinner with data using room, I'm getting no errors but my spinner isn't displaying anything. I think it might have something to do with how I'm calling initFirstUnitSpinnerData() in my onCreateView method? But I'm having no luck. I'm using kotlin.
Thanks in advance.
DAO:
#Query("SELECT firstUnit FROM conversion_table WHERE category LIKE :search")
fun getByCategory(search: String): LiveData<List<String>>
Repository:
fun getByCategory(search: String): LiveData<List<String>>{
return conversionsDAO.getByCategory(search)
}
View Model:
fun getByCategory(search: String): LiveData<List<String>> {
return repository.getByCategory(search)
}
Fragment:
class UnitsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
private lateinit var mConversionsViewModel: ConversionsViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_units, container, false)
mConversionsViewModel = ViewModelProvider(this).get(ConversionsViewModel::class.java)
initFirstUnitSpinnerData()
return view
}
private fun initFirstUnitSpinnerData() {
val spinnerFirstUnit = view?.findViewById<Spinner>(R.id.firstUnitSpinner)
if (spinnerFirstUnit != null) {
val allConversions = context?.let {
ArrayAdapter<Any>(it, R.layout.support_simple_spinner_dropdown_item)
}
mConversionsViewModel.getByCategory("Distance")
.observe(viewLifecycleOwner, { conversions ->
conversions?.forEach {
allConversions?.add(it)
}
})
spinnerFirstUnit.adapter = allConversions
spinnerFirstUnit.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Toast.makeText(requireContext(), "$allConversions", Toast.LENGTH_LONG).show()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
}
This is the kind of thing you should debug really - click on the left gutter for the first line of initFirstUnitSpinnerData (the val spinnerFirstUnit one), click the Debug App button up near the Run one, and it'll pause when it hits that breakpoint you added.
Then you can move through, step by step, looking at the values of stuff and checking if it looks right, and how the code executes. It's a super useful thing to learn and it'll save you a lot of headaches!
Anyway I'm guessing your problem is that you're calling initFirstUnitSpinnerData from inside onCreateView - the latter is called by the Fragment when it needs its layout view inflating, which you do and then return it to the Fragment.
So inside initFirstUnitSpinnerData, when you reference view (i.e. the Fragment's view, which it doesn't have yet, because onCreateView hasn't returned it yet) you're getting a null value. So spinnerFirstUnit ends up null, and when you null check that, it skips setting up the adapter.
Override onViewCreated (which the Fragment calls when it has its layout view) and call your function from there, it'll be able to access view then - see if that helps!

Fragment. getViewLifeCycleOwner doesn't prevent multiple calls of LiveData Observer

I use Clean Architecture, LiveData, Navigation component & Bottom Navigation view.
I am creating a simple application with three tabs. By default, the First tab Fragment loads user data using some API. When i go to another tabs and then return to the First tab Fragment, i see, that observe return a new data!
I need observe not to return data again when I switch back to the first tab! what am I doing wrong? Could you help me please?
P.s. For navigation i use sample from navigation-advanced-sample and after switching tabs onDestroy is not called.
First solution in the article Observe LiveData from ViewModel in Fragment said:
One proper solution is to use getViewLifeCycleOwner() as LifeCycleOwer while observing LiveData inside onActivityCreated as follows.
I use following code, but it's not work for me:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
}
Second solution in the article Architecture Components pitfalls — Part 1 recommends using Resetting an existing observer and Manually unsubscribing the observer in onDestroyView(). But it doesn't work for me either...
ProfileFragment.kt
class ProfileFragment : DaggerFragment() {
#Inject
lateinit var viewModel: ProfileFragmentViewModel
private val observer = Observer<Resource<Profile>> {
when (it.status) {
Resource.Status.LOADING -> {
Timber.i("Loading...")
}
Resource.Status.SUCCESS -> {
Timber.i("Success: %s", it.data)
}
Resource.Status.ERROR -> {
Timber.i("Error: %s", it.message)
}
}
};
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Timber.d("onCreateView")
return inflater.inflate(R.layout.fragment_profile, container, false)
}
fun <T> LiveData<T>.reObserve(owner: LifecycleOwner, observer: Observer<T>) {
removeObserver(observer)
observe(owner, observer)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.d("onViewCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
// viewModel.getProfileLive().reObserve(viewLifecycleOwner, observer)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
}
override fun onDestroyView() {
super.onDestroyView()
Timber.d("onDestroyView")
// viewModel.getProfileLive().removeObserver(observer)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("onDestroy")
}
override fun onDetach() {
super.onDetach()
Timber.d("onDetach")
}
}
ProfileFragmentViewModel.kt
class ProfileFragmentViewModel #Inject constructor(
private val profileUseCase: ProfileUseCase
) : ViewModel() {
init {
Timber.d("Init profile VM")
}
fun getProfileLive() = profileUseCase.getProfile()
}
ProfileUseCase
class ProfileUseCase #Inject constructor(
private val profileRepository: ProfileRepository
) {
fun getProfile(): LiveData<Resource<Profile>> {
return profileRepository.getProfile()
}
}
ProfileRepository.kt.
class ProfileRepository #Inject constructor(
private val loginUserDao: LoginUserDao,
private val profileDao: ProfileDao,
) {
fun getProfile(): LiveData<Resource<Profile>> =
liveData(Dispatchers.IO)
{
emit(Resource.loading(data = null))
val profile = profileDao.getProfile()
// Emit Success result...
}
}
It's because of how Fragment Lifecycle works. When you move to and fro from a fragment onViewCreated() is called again. In onViewCreated you're calling viewModel.getProfileLive() which returns the livedata upto from the repository and observe to it.
Since onViewCreated() gets called everytime when you move back to the Fragment so is your call to viewModel.getProfileLive() and in turn the repository gets called again which again triggers the observe method in your Fragment.
In order to solve this problem,
create a LiveData variable in your ViewModel, set it to the returned Live Data from Repository.
In the Fragment observe to the LiveData variable of your ViewModel not the one returned from Repository.
That way, your observe method will get triggered on very first time and only when value of your data from repository changes.

Why LiveData is slow

In my application, I load data from the local database and it's very slow. I tried to find what is slow and I found that this occurs because of LiveData.
I created a sample application to test LiveData speed here you are my test code:
FirstFragment:
class FirstFragment : Fragment(), FirstFragmentCallback {
private val TAG = FirstFragment::class.java.simpleName
private var mViewModel: FirstFragmentViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = ActivityUtils.obtainViewModel(requireActivity(), FirstFragmentViewModel::class.java)
(mViewModel as FirstFragmentViewModel).callback = this
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.first_fragment, container, false)
Log.d(TAG, "onCreateView called")
registerObservables()
mViewModel?.loadData()
return v
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val secondFragmentButton = view.findViewById<Button>(R.id.second)
secondFragmentButton.setOnClickListener {
ActivityUtils.replaceFragmentInActivity(requireFragmentManager(), SecondFragment(), R.id.container, false)
}
}
private fun registerObservables(){
mViewModel?.dataLoadedEvent?.observe(this, Observer {
Log.d(TAG, "dataLoaded event")
})
mViewModel?.dataLoaded2Event?.observe(this, Observer {
Log.d(TAG, "dataLoaded2 event")
})
}
override fun dataLoaded() {
Log.d(TAG, "dataLoaded callback")
}
}
FirstFragmentViewModel:
class FirstFragmentViewModel(val mAppliction: Application): AndroidViewModel(mAppliction) {
val dataLoadedEvent: SingleLiveEvent<Void> = SingleLiveEvent()
val dataLoaded2Event: MutableLiveData<Void> = MutableLiveData()
var callback: FirstFragmentCallback? = null
fun loadData(){
dataLoadedEvent.postValue(null)
dataLoaded2Event.postValue(null)
callback?.dataLoaded()
}
}
If I run this fragment I got these in the LogCat:
2019-05-15 13:23:07.405 8632-8632/livedatatest D/FirstFragment: onCreateView called
2019-05-15 13:23:07.406 8632-8632/livedatatest D/FirstFragment: dataLoaded callback
2019-05-15 13:23:07.438 8632-8632/livedatatest D/FirstFragment: dataLoaded event
2019-05-15 13:23:07.439 8632-8632/livedatatest D/FirstFragment: dataLoaded2 event
You can see that dataLoadedEvent.postValue(null) take at least 30ms, but the simple callback is called immediately.
Is there any solution to speed up LiveData events?
You can see that dataLoadedEvent.postValue(null) take at least 30ms
postValue() is for when you want to update the MutableLiveData from a background thread. Under the covers, it uses a Handler to route your event to the main application thread. Therefore, there will be some delay, as other main application thread work queue events get processed.
It also illustrates that your benchmark is flawed ("comparing apples to oranges"). Either:
Use Handler instead of a callback (or some other "run this code on the main application thread" approach), or
Use setValue() (or value= since you are in Kotlin) instead of postValue(), to update the MutableLiveData content directly on the main application thread

Categories

Resources