Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State Management in Android Applications

State Management in Android Applications

Looking through why saving navigation state and screen state is important for users and developers, and what "edge-case" looking behaviors we need to take into consideration when developing Android applications.

Avatar for Gabor Varadi

Gabor Varadi

May 09, 2019
Tweet

More Decks by Gabor Varadi

Other Decks in Programming

Transcript

  1. Core App Quality Guidelines From https://developer.android.com/docs/quality-guidelines/core-app-quality#fn „When returning to the

    foreground, the app must restore the preserved state and any significant stateful transaction that was pending, such as changes to editable fields, game progress, menus, videos, and other sections of the app.
  2. How to induce process death? • Step 1: put app

    in background with HOME • Step 2: press „Terminate application” • Step 3: restart app from launcher
  3. Expecting Singletons / statics to survive class MainActivity : AppCompatActivity()

    { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) fab.setOnClickListener { view -> ObjectHolder.myObject = MyObject("someName", 27) startActivity(intentFor<SecondActivity>()) } } } class SecondActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) inputName.text = ObjectHolder.myObject.name } }
  4. 05-08 14:05:39.845 3601-3601/com.zhuinden.processdeathexample D/AndroidRuntime: Shutting down VM 05-08 14:05:39.845 3601-3601/com.zhuinden.processdeathexample

    E/AndroidRuntime: FATAL EXCEPTION: main Process: com.zhuinden.processdeathexample, PID: 3601 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.zhuinden.processdeathexample/com.zhuinden.processdeathexample.SecondA ctivity}: kotlin.UninitializedPropertyAccessException: lateinit property myObject has not been initialized at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476) Caused by: kotlin.UninitializedPropertyAccessException: lateinit property myObject has not been initialized at com.zhuinden.processdeathexample.ObjectHolder.getMyObject(ObjectHolder.kt:4) at com.zhuinden.processdeathexample.SecondActivity.onCreate(SecondActivity.kt:12) Expecting Singletons / statics to survive
  5. „savedInstanceState == null for the first launch”? override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState); // „don’t refetch data after rotation” if (savedInstanceState == null) { loadData(); } // ... }
  6. „savedInstanceState == null for the first launch” -- nope HOWEVER:

    • Fragments ARE recreated by super.onCreate() • The first fragment MUST be added in a if(savedInstanceState == null) check
  7. FragmentPagerAdapter.getItem()? private fun initFragment() { homeFragment = HomeScreenFragment.newInstance() incidentFragment =

    IncidentFragment.newInstance() weatherFragment = WeatherFragment.newInstance() val fm = supportFragmentManager viewPager.adapter = object: FragmentPagerAdapter(fm) { override fun getItem(position: Int): Fragment = when (position) { 0 -> homeFragment 1 -> incidentFragment 2 -> weatherFragment } } }
  8. Should probably be „createFragment” Process: XXXXXX, PID: 3192 kotlin.UninitializedPropertyAccessException: lateinit

    property has not been initialized at xxx.ui.IncidentScreenFragment.showData(IncidentScreenFragment.kt:78) at xxx.ui.home.HomeActivity.filterData(HomeActivity.kt:110)
  9. Should probably be „createFragment” Fixed version: viewPager.adapter = object: FragmentPagerAdapter(fm)

    { override fun getItem(position: Int): Fragment = when (position) { 0 -> HomeFragment.newInstance() 1 -> IncidentFragment.newInstance() 2 -> WeatherFragment.newInstance() } }
  10. Don’t trust savedInstanceState after Fragment.onCreate • The savedInstanceState inside onCreateView

    and after can be out of date • onSaveInstanceState to Bundle is not called when a Fragment is detach()/attach()ed
  11. What needs to be persisted? • Navigation state is already

    managed by the system on Android out of the box* – Empty ctor + using intent extras / fragment arguments • Screen state is partially managed by the system – Views with IDs have their state persisted – Complex state (f.ex. RecyclerView selection) are not persisted automatically – Dynamically added views should be recreatable
  12. Example for saving/restoring state override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    if (savedInstanceState != null) { selectedSportId = savedInstanceState.getLong("selectedSportId") selectedPosition = savedInstanceState.getInt("selectedPosition") selectedTags.clear() selectedTags.addAll( savedInstanceState.getStringArrayList("selectedTags")) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong("selectedSportId", selectedSportId) outState.putInt("selectedPosition", selectedPosition) outState.putStringArrayList("selectedTags", ArrayList(selectedTags)) }
  13. What SHOULDN’T be persisted? • Data – Bundle has a

    size limit – Data should be fetched asynchronously, off the UI thread • Transient state – „Loading” state: computed from progress of side- effect („something is happening”, but is it really?)
  14. Single object state! @Parcelize data class PersonViewState( // loading should

    come from elsewhere val personFilter: String = "", val unhandledError: ErrorType? = null // enum or sealed class ): Parcelable ...but is this really necessary?
  15. Loading data • Asynchronous loading should either begin on initialization,

    or when observed • Jetpack: store LiveData inside ViewModel, and expose it to observers – fetch begins when observed • Data can be loaded via a Transformation chain from a MutableLiveData that stores the state – changes trigger new data load • Note: LiveData is analogous with BehaviorRelay
  16. Saving state of a ViewModel class MainViewModel: ViewModel() { private

    val _selectedPosition = MutableLiveData<Int>() fun saveState(bundle: Bundle) { bundle.putInt("selectedPosition", _selectedPosition.value) } fun restoreState(bundle: Bundle) { _selectedPosition.value = bundle.getInt("selectedPosition") } }
  17. class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProviders.of(this, object: ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T = MainViewModel().let { viewModel -> savedInstanceState?.getBundle("viewModelState")?.let { bundle -> viewModel.restoreState(bundle) } } as T }).get(MainViewModel::class.java) ... } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val viewModelState = Bundle().also { bundle -> viewModel.saveState(bundle) } outState.putBundle("viewModelState", viewModelState) } }
  18. Jetpack: SavedState private val mainViewModel by viewModels { ... }

    class MainViewModel( private val sportDao: SportDao private val handle: SavedStateHandle ): ViewModel() { private val _selectedSportId = handle.getLiveData("selectedSportId") private val _selectedPosition = handle.getLiveData("selectedPosition") val sport = _selectedSportId.switchMap { sportDao.getSport(it) } val selectedPosition: LiveData<Int> get() = _selectedPosition }
  19. Jetpack: SavedState class MainViewModelFactory( private val sportDao: SportDao, owner: SavedStateRegistryOwner,

    defaultState: Bundle? ): AbstractSavedStateVMFactory(owner, defaultState) { override fun <T: ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T = MainViewModel(sportDao, handle) as T }
  20. Talks to watch Fred Porciúncula: A Dagger Journey https://www.youtube.com/watch?v=9fn5s8_CYJI Fun

    with LiveData (Android Dev Summit '18) https://www.youtube.com/watch?v=2rO4r-JOQtA Google I/O Day 2: What’s new in Architecture Components https://www.youtube.com/watch?v=Qxj2eBmXLHg&list=PLOU2XLYxms ILVTiOlMJdo7RQS55jYhsMi&index=29