Tengo un problema con una aplicación que usa ViewPager para mostrar fragmentos. Todo funciona bien hasta que la aplicación pasa a segundo plano y se mata desde el sistema operativo. Parece que después de la restauración tengo 2 IncidentScreenFragment que controlan los eventos, uno con un presentador nulo (MVP) que bloquea mi aplicación.
My HomeActivity se ve así:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter.onViewCreated()
initViews(savedInstanceState)
}
private fun initViews(savedInstanceState: Bundle?){
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)
initFragment()
initMenu()
}
private fun initFragment(){
homeFragment = HomeScreenFragment.newInstance()
incidentFragment = IncidentScreenFragment.newInstance()
chatFragment = ChatFragment.newInstance()
weatherFragment = WeatherFragment.newInstance()
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, this)
viewPager.offscreenPageLimit = 4
viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {bottom_navigation.currentItem = position}
})
}
override fun getFragmentByPos(pos: Int): Fragment {
return when(pos){
0 -> homeFragment
1 -> incidentFragment
2 -> chatFragment
3 -> weatherFragment
else -> {
homeFragment
}
}
}
Y mi adaptador:
class ViewPagerAdapter internal constructor(fm: FragmentManager, activity:infinite_software.intelligence_center.intelligencecenter.ui.home.FragmentManager) : FragmentPagerAdapter(fm) {
private val COUNT = 4
private val activity = activity
override fun getItem(position: Int): Fragment{
var fragment: Fragment? = null
when (position) {
0 -> fragment = activity.getFragmentByPos(0)
1 -> fragment = activity.getFragmentByPos(1)
2 -> fragment = activity.getFragmentByPos(2)
3 -> fragment = activity.getFragmentByPos(3)
}
return fragment!!
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
}
override fun getCount(): Int {
return COUNT
}
override fun getPageTitle(position: Int): CharSequence? {
return "Section " + (position + 1)
}
}
Cada fragmento tiene un método estático que devuelve un nuevo fragmento:
companion object {
fun newInstance(): HomeScreenFragment {
return HomeScreenFragment()
}
}
Cuando la aplicación ha sido eliminada en segundo plano, me doy cuenta de que hay 2 objetos (Fragmento) que escuchan el evento, uno con El presentador instancia correctamente y otro sin él.
Debajo de mi clase abstracta BaseFragment:
abstract class BaseFragment<P : BasePresenter<BaseView>> : BaseView,Fragment() {
protected lateinit var presenter: P
override fun getContext(): Context {
return activity as Context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter = instantiatePresenter()
}
override fun showError(error: String) {
(activity as BaseActivity<BasePresenter<BaseView>>).showError(error)
}
override fun showError(errorResId: Int) {
(activity as BaseActivity<BasePresenter<BaseView>>).showError(errorResId)
}
abstract fun onBackPressed(): Boolean
/**
* Instantiates the presenter the Fragment is based on.
*/
protected abstract fun instantiatePresenter(): P
abstract val TAG: String
Código de fragmento de incidente:
class IncidentScreenFragment: BaseFragment<IncidentScreenPresenter>(), BaseView, IncidentView, AlertFilterListener, AlertItemClickListener, IncidentDetailListener {
var rvAdapter : IncidentAdapter? = null
var state : Int = LIST_STATE
override fun instantiatePresenter(): IncidentScreenPresenter {
return IncidentScreenPresenter(this)
}
override val TAG: String
get() = "INCIDENT"
override fun getContext(): Context {
return activity as Context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_incident, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
presenter.onViewCreated()
initObserve()
}
private fun initViews(){
//Reclycler view
alertRV.layoutManager = LinearLayoutManager(context)
rvAdapter = IncidentAdapter(ArrayList(), context, this)
alertRV.adapter = rvAdapter
//Apply Listeners
headerBox.setFilterListener(this)
incidentDetailView.setListener(this)
}
override fun initObserve() {
//Init observe presenter model
val alertObserver = Observer<ArrayList<AlertModel>> { alerts ->
Timber.d("Data received from Presenter [$alerts]")
showAlertList(alerts)
}
presenter.filteredAlertList.observe(context as BaseActivity<BasePresenter<BaseView>>,alertObserver)
}
override fun updateThisFilters(boxState: Boolean, level: Int) {
presenter.updateFilterList(boxState,level)
}
fun showOnlyThisLevel(level:Int){
presenter.showOnlyThisLevel(level)
headerBox.disableBoxExcept(level)
}
fun showAlertList(list: ArrayList<AlertModel>){
rvAdapter?.updateData(list)
}
override fun onItemClick(model: AlertModel) {
presenter.loadAlertDetail(model)
}
override fun showAlertDetail(model: AlertModel) {
incidentDetailView.setUpFromModel(model)
WhiteWizard.slideLeftEffect(incidentDetailView,incidentListRootElement)
state = DETAIL_STATE
}
override fun onbackFromDetailPressed() {
WhiteWizard.slideRightEffect(incidentListRootElement,incidentDetailView)
state = LIST_STATE
}
override fun showLoader() {
loaderIncident.visibility = View.VISIBLE
}
override fun hideLoader() {
loaderIncident.visibility = View.INVISIBLE
}
override fun onBackPressed(): Boolean {
when(state){
LIST_STATE -> return false
DETAIL_STATE -> {
onbackFromDetailPressed()
return true
}
else -> return false
}
}
fun newInstance(): IncidentScreenFragment {
return IncidentScreenFragment()
}
}
Cuando hago clic en el botón en homePage para mostrar el contenido del fragmento, obtuve:
Process: XXXXXX, PID: 3192
kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
at infinite_software.intelligence_center.intelligencecenter.base.BaseFragment.getPresenter(BaseFragment.kt:11)
at XXXXXX.ui.home.incidentScreen.IncidentScreenFragment.showOnlyThisLevel(IncidentScreenFragment.kt:78)
at XXXXXX.ui.home.HomeActivity.filterDataWithSeverity(HomeActivity.kt:110)
at XXXXXX.ui.home.homeScreen.HomeScreenFragment.filterBy(HomeScreenFragment.kt:76)
at XXXXXX.ui.home.homeScreen.HomeScreenFragment$initViews$5.onClick(HomeScreenFragment.kt:56)
Si intento imprimir el identificador de Fragment, obtengo 2 identificadores diferentes de la llamada al método showOnlyThisLevel() y onBackPressed(). ¿Qué echo de menos?
Solución
Después de investigar un poco, parece que el problema se deriva del nombre erróneo del método: ser nombrado, pero no especificar claramente que se supone que el método abstracto devuelve una nueva instancia de un fragmento en lugar de simplemente «obtener una instancia de uno».FragmentPagerAdapter
getItem()
getItem(int position)
Por supuesto, no hay mucho que podamos hacer con respecto a un nombre incorrecto después de que haya estado en la naturaleza durante 7 años, pero al menos podemos corregir el error que se deriva de este problema en su código 😉
Sin más preámbulos, la causa de su NPE es que (donde se crea una instancia de su presentador) nunca se llama.onCreateView
Esto sucede porque está creando el fragmento aquí:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
homeFragment = HomeScreenFragment.newInstance()
incidentFragment = IncidentScreenFragment.newInstance()
}
Devuelve este fragmento desde dentro en su FragmentPagerAdapter:getItem(int position)
override fun getItem(position: Int): Fragment = when(position) {
...
1 -> activity.incidentFragment
...
}
Así que lo que sabemos es que en él, nunca se llama.activity.incidentFragment
onCreateView()
Esto se debe al hecho de que en realidad nunca se agrega a un FragmentManager y nunca se muestra en la pantalla.
Esto se debe a que en Activity recrea todos los fragmentos, utilizando su constructor no-args, a través de la reflexión, mientras mantiene su etiqueta (consulte findFragmentByTag
).super.onCreate(savedInstanceState)
Así que como puedes ver en esta respuesta, o como puedo citar aquí:
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
Solo se llama al método si la etiqueta de fragmento que FragmentPagerAdapter establece para el fragmento no encuentra el fragmento, que se vuelve a crear automáticamente después de que la condición de memoria baja mata la aplicación.getItem(position)
Por lo tanto, SU nuevo fragmento (que crea a mano en la Actividad) NUNCA se usa y, por lo tanto, no tiene vista, nunca se inicializa, nunca se agrega a FragmentManager, no es la misma instancia que lo que realmente está dentro de su ViewPager, y se bloquea cuando lo llama. ¡Auge!
La solución es crear una instancia del fragmento dentro del método getItem(position)
de FragmentPagerAdapter. Para obtener una instancia del fragmento, utilice esta respuesta.