Can't access fragment's method from activity - UninitializedPropertyAccessException - android

I have a ViewPager. I want to change page when fragment allow it to do.
This is the onClick when I want to check.
when (currentPage) {
0 -> {
if ((mAdapter.getItem(currentPage) as NameFragment).canGo()) {
mViewPager.setCurrentItem(currentPage + 1, true)
}
}
1 -> ...
}
My Fragment:
private lateinit var fName: EditText
private lateinit var lName: EditText
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = UI {
verticalLayout {
fName = editText {
hint = "FIRST NAME"
textSize = 20f
}
lName = editText {
hint = "LAST NAME"
textSize = 20f
}
}
}
fun canGo(): Boolean {
val firstName = fName.text.toString()
val lastName = lName.text.toString()
if (firstName.isEmpty() || firstName.isBlank() || lastName.isBlank() || lastName.isEmpty()) {
toast("First name or Last name cannot be empty")
return false
}
return true
}
I initialize fName and lName on onCreateView method and this is the error I get. Full exception is here.
kotlin.UninitializedPropertyAccessException: lateinit property fName has not been initialized

These properties need to be initialised in the onViewCreated method, not onCreateView.
onCreateView is only responsible for creating the view as the name suggests, while onViewCreated is responsible for handling events after your fragment is properly created.
And if you need, you can create a callback that will be triggered when your fragment view is created, then you implement that callback in the activity, so that you will only access that canGo method after the fragment is created. Again, only if you really need to.
interface OnFragmentInflatedListener {
fun onFragmentInflated(fragment: Fragment)
}
// Fragment
private var onFragmentInflatedListener: OnFragmentInflatedListener? = null
fun setOnFragmentInflatedListener(onFragmentInflatedListener: OnFragmentInflatedListener?) {
this.onFragmentInflatedListener = onFragmentInflatedListener
}
// Fragment's onViewCreated
// initialise your properties here
onFragmentInflatedListener?.onFragmentInflated(this)
// Activity
class MyActivity : AppCompatActivity(), OnFragmentInflatedListener {
#Override
fun onCreate(savedInstanceState: Bundle) {
// do your stuff
myFragment.setOnFragmentInflatedListener(this)
}
#Override
fun onFragmentInflated(fragment: Fragment) {
// do what you wanted to do
}
}

I found the answer on this article. First I edited my Adapter:
private class Adapter(fm: FragmentManager, private val fragments: ArrayList<Fragment>) : FragmentStatePagerAdapter(fm) {
private val instantiatedFragments = SparseArray<WeakReference<Fragment>>()
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount() = fragments.size
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as Fragment
instantiatedFragments.put(position, WeakReference(fragment))
return fragment
}
#Nullable
fun getFragment(position: Int) = instantiatedFragments.get(position)?.get()
}
And also my onClick method
when (currentPage) {
0 -> {
if ((mAdapter.getFragment(currentPage) as NameFragment).canGo()) {
mViewPager.setCurrentItem(currentPage + 1, true)
}
}
1 -> ...
}
I changed getItem(currentPage) to getFragment(currentPage).

Related

TextView.text changes but display of its display on a fragment isn't updating

I had a working app that does some arithmetic functionality that is out of the scope of the question, then I wanted to add more functionality to it, so i separated the layout into activity and fragment in order to later add other fragments that will do extra functions.
yet when I separated the layout taking some buttons along with a TextView (R.id.Result) to the new fragment, the text property of the TextView still updates as expected, but the display stays the same, always showing the initialization value initially assigned to it on its creation time.
I confirmed that the objects are the same as I expected them to be during runtime verified through logcat, what I need OFC is for the TextView display to update when I change its text property, numberInsertAction is called from the buttons properly and send proper data.
Important Note: below is only the relevant parts of code, it is much larger and I know what you see below can be simplified but it is built this way because of other classes and functionality that aren't shown below, if you need to see or ask about something outside the below code please do, yet again I only included the related part only and removed the business functionality.
Thanks in advance.
just to reiterate: numberInsertAction(view: View) is the entry point/function called by the buttons on the fragment.
MainActivity.kt
class MainActivity : AppCompatActivity(), AddObserverToActivity {
private lateinit var binding: ActivityMainBinding
private lateinit var stateManager: StateManager
override fun onCreate(savedInstanceState: Bundle?) {
//initialize layout
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val activityRoot = binding.root
setContentView(activityRoot)
stateManager = StateManager()
}
override fun addResultObserver(observer: Observer) {
Log.d(TAG, "addObserver! ${observer.toString()} ${observer::class.toString()}")
StateManager.addDisplayObserver(observer)
}
fun numberInsertAction(view: View) {
if (view is Button) {
StateManager.enterDigit(view.text.toString())
}
}
}
CalculatorFragment.kt
class CalculatorFragment : Fragment() {
companion object {
fun newInstance() = CalculatorFragment()
}
private lateinit var binding: FragmentCalculatorBinding
private lateinit var mainActivityHandle: AddObserverToActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG, "onCreateView")
binding = FragmentCalculatorBinding.inflate(inflater, container, false)
return inflater.inflate(R.layout.fragment_calculator, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "using on view created")
mainActivityHandle = context as AddObserverToActivity
Log.d(TAG, "${binding.Result} ${(binding.Result)::class.simpleName.toString()}")
Log.d(TAG, mainActivityHandle::class.toString())
mainActivityHandle.addResultObserver(DisplayPanel(binding.Result))
}
}
StateManager.kt
class StateManager : Observable() {
private val displayBuffer = DisplayBuffer(DecimalVariable("0"))
fun enterDigit(digit: String) {
Log.d(TAG, "enterDigit: $digit, $currentState")
displayBuffer.insertDigit(digit)
}
fun addDisplayObserver(observer: Observer) {
Log.d(TAG, "addDisplayObserver: $observer")
displayBuffer.addObserver(observer)
}
private fun doNotify(Notified: Any) {
Log.d(TAG, "doNotify: $Notified")
setChanged()
notifyObservers(Notified)
}
}
DisplayBuffer.kt
class DisplayBuffer(initializationValue: SomeClass) : Observable() {
private var initialValue = initializationValue
private var resultString = "0"
var value = initialValue
set(value) {
Log.d(TAG, "setter: $value")
field = value
doNotify()
}
fun set(value: String) {
Log.d(TAG, "set: $value")
this.value = value as Int
}
private fun doNotify() {
Log.d(TAG, "doNotify")
setChanged()
notifyObservers(value.toString())
}
fun insertDigit(digit: String) {
Log.d(TAG, "insertDigit: $digit result: $resultString")
resultString = resultString + digit
Log.d(TAG, "new value: $resultString")
setChanged()
notifyObservers(resultString)
}
}
DisplayPanel.kt
class DisplayPanel(calculationTextView: TextView) : Observer {
private val displayField: TextView = calculationTextView
private val maxDigits = 16
private fun setDisplay(text: String) {
Log.d(TAG, "setDisplay: $text")
if (text.length <= maxDigits) {
displayField.text = text
//displayField.invalidate()
}
}
override fun update(observable: Observable?, targetObjects: Any?) {
Log.d(TAG, "update: $this $observable, $targetObjects")
setDisplay(targetObjects as String)
}
}
Add binding.lifecycleOwner = viewLifecycleOwner in onCreateView or onViewCreated method.
was answered by #Mike M in Comments:
In CalculatorFragment,
He instructed me to change
return inflater.inflate(R.layout.fragment_calculator, container, false) to return binding.root.
as the problem was that this function inflated two instances of the fragment calculator layout and returned the later while it used the former as observer.
to qoute #Mike-M:
The inflater.inflate() call is creating a new instance of that layout that is completely separate from the one that FragmentCalculatorBinding is creating and using itself.
FragmentCalculatorBinding is inflating the view internally, which is why it is passed the inflater in its inflate() call.

Why return function in variable is null? Kotlin + Android

I am missing some basic coding knowledge here I think, I want to present value to the fragment by assigning the function to the variable in a viewModel. When I call the function directly, I get correct value. When I assign function to variable and pass the variable to the fragment it is always null, why?
View Model
class CartFragmentViewModel : ViewModel() {
private val repository = FirebaseCloud()
private val user = repository.getUserData()
val userCart = user?.switchMap {
repository.getProductsFromCart(it.cart)
}
private fun calculateCartValue(): Long? {
val list = userCart?.value
return list?.map { it.price!! }?.sum()
}
//val cartValue = userCart?.value?.sumOf { it.price!! } <- THIS will be null
val cartValue = calculateCartValue() <- THIS will be null
val cartSize = userCart?.value?.size <- THIS will be null
}
Fragment
class CartFragment : RootFragment(), OnProductClick, View.OnClickListener {
private lateinit var cartViewModel: CartFragmentViewModel
private lateinit var binding: FragmentCartBinding
private val cartAdapter = CartAdapter(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_cart,
container,
false
)
setAnimation()
cartViewModel = CartFragmentViewModel()
binding.buttonToCheckout.setOnClickListener(this)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerCart.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = cartAdapter
}
cartViewModel.userCart?.observe(viewLifecycleOwner, { list ->
cartAdapter.setCartProducts(list)
updateCart()
})
}
override fun onClick(view: View?) {
when (view) {
binding.buttonToCheckout -> {
navigateToCheckout(cartViewModel.cartValue.toString())
cartViewModel.sendProductEvent(
cartAdapter.cartList,
ProductEventType.CHECKOUT
)
}
}
}
override fun onProductClick(product: Product, position: Int) {
cartViewModel.removeFromCart(product)
cartAdapter.removeFromCart(product, position)
updateCart()
}
private fun updateCart() {
binding.textCartTotalValue.text = cartViewModel.cartValue.toString() <- NULL
binding.textCartQuantityValue.text = cartViewModel.cartSize.toString() <- NULL
}
}
Thanks!
It looks like userCart is some sort of observable variable which initially holds a null value and then gets populated with the data from your repository after the network call (or something similar) completes.
The reason that all your variables are null are because you are declaring their value immediately, so by the time those statements get executed, the network call hasn't yet completed and userCart?.value is null. However calling the calculateCartValue() function later on in the code might yield a value if the fetch is complete.

PresentationFragment inflates slow in android

MyActivity is...
private var preso: ClientResultPresentationFragment? = null
private var presoHelper: PresentationHelper? = null
private val presoListener = object: PresentationHelper.Listener{
override fun initSecondDisplay(display: Display?) {
Log.d("preso", "initSecondDisplay()")
preso = MytPresentationFragment.newInstance(this#MyActivity, display)
preso!!.show(fragmentManager, PRESO) // PRESO is a static value.
}
override fun clearPreso(switchToInline: Boolean) {
if (preso != null) {
preso!!.dismiss()
preso = null
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// Must open second display at the same time.
onCreatePreso()
refinedResultData = intent.extras!!.getParcelable(RESULT_DATA)
Log.d("result", "${resultData.toString()}")
init(resultData!!)
}
private fun init(data: ResultData){
initView(data)
preso!!.syncData(data)
}
private initView(data: ResultData){
// TODO: initViews...
}
private fun onCreatePreso(){
presoHelper = PresentationHelper(this, presoListener)
presoHelper!!.enable()
}
override fun onResume() {
super.onResume()
presoHelper?.onResume()
}
override fun onPause() {
presoHelper?.onPause()
super.onPause()
}
My Presentation Fragment is...
var mView: View? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("preso", "onCreateView()")
mView = LayoutInflater.from(context).inflate(R.layout.fragment_my, container, false)
return mView
}
fun syncData(data: ResultData){
Log.d("preso", "syncData()->${data.toString()}")
initView()
// return >>> here...
mView!!.tv_title.text = "${data.title}" // <<< crash here >>>
// TODO: set initial data
}
fun initView(){
// initViews...
}
companion object {
fun newInstance(context: Context?, display: Display?): MyPresentationFragment {
val frag = MyPresentationFragment()
frag.setDisplay(context, display)
return frag
}
}
And the log is...
"resultData.toString()"
initSecondDisplay()
"syncData()->${data.toString()}"
crash------------------
if I just uncomment return part in syncData() of Presentation Fragment(It will not access the views)
"resultData.toString()"
initSecondDisplay()
"syncData()->${data.toString()}"
onCreateView()
So, syncData() is called earlier than onCreateView(). What should I do? I moved preso!!.syncData() after preso!!.show(fragmentManager, PRESO) and it's the same.
Since FragmentManager operations is async, you should send a callback from your fragment to the activity in the fragment OnViewCreated or maybe OnResume and only then send the data from the activity to the fragment.
Or you can just use ViewModel and LiveData to provide your data to fragments

ViewPager in Fragment causes Fragments to be loaded twice on orientation change

I have a fragment class that contains a ViewPager with two sites. These sites contain a few widgets like CheckBoxes and I want them to stay checked when the orientation changes.
MainFragment:
class StatsFragment: Fragment() {
private val fragmentStatsCat = StatsCategoryFragment()
private val fragmentStatsMon = StatsMonthFragment()
private lateinit var pager: ViewPager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_stats, container, false)
pager = view.findViewById(R.id.stats_container)
val pagerAdapter = StatsScreenSlidePagerAdapter(childFragmentManager)
pager.adapter = pagerAdapter
return view
}
private inner class StatsScreenSlidePagerAdapter(fm: FragmentManager): FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return if (position == 0) {
fragmentStatsCat
} else {
fragmentStatsMon
}
}
override fun getCount(): Int = 2
override fun saveState(): Parcelable? {
return null
}
}
}
One of the ViewPagerFragments:
class StatsMonthFragment: Fragment() {
companion object {
private const val CHECKBOX_KEY = "isChecked"
private const val CATEGORY_KEY = "category"
}
private lateinit var btnPrevCat: ImageButton
private lateinit var btnNextCat: ImageButton
private lateinit var cbAllCats: CheckBox
private lateinit var categories: List<Category>
private var i: Int = 0
private var isCbChecked: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState != null) {
i = savedInstanceState.getInt(CATEGORY_KEY)
isCbChecked = savedInstanceState.getBoolean(CHECKBOX_KEY)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_stats_months, container, false)
btnNextCat = view.findViewById(R.id.btn_next_cat)
btnPrevCat = view.findViewById(R.id.btn_prev_cat)
cbAllCats = view.findViewById(R.id.cb_all_cats)
categories = DatabaseInitializer.getInstance().getAllCategories(AppDatabase.getInstance(context).categoryDao())
if(isCbChecked == null || isCbChecked!!) {
val initFragment = StatsSelectedFilterFragment() //This is just a fragment with a TextView; I use fragments for that for the CustomAnimations
initFragment.setText(categories[i].cat_name)
childFragmentManager.beginTransaction().add(R.id.container_stats_categories, initFragment).commit()
} else {
val fragment = StatsSelectedFilterFragment()
fragment.setText(getString(R.string.all_categories))
childFragmentManager.beginTransaction()
.replace(R.id.container_stats_categories, fragment)
.commit()
}
btnNextCat.setOnClickListener {
val fragment = StatsSelectedFilterFragment()
fragment.setText(categories[i].cat_name)
childFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left)
.replace(R.id.container_stats_categories, fragment)
.commit()
}
btnPrevCat.setOnClickListener {
val fragment = StatsSelectedFilterFragment()
fragment.setText(categories[i].cat_name)
childFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.container_stats_categories, fragment)
.commit()
}
cbAllCats.setOnCheckedChangeListener{_,checked->
if(checked) {
val fragment = StatsSelectedFilterFragment()
fragment.setText(getString(R.string.all_categories))
childFragmentManager.beginTransaction()
.replace(R.id.container_stats_categories, fragment)
.commit()
} else {
val fragment = StatsSelectedFilterFragment()
fragment.setText(categories[i].cat_name)
childFragmentManager.beginTransaction()
.replace(R.id.container_stats_categories, fragment)
.commit()
}
}
return view
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val isAllCatsChecked = cbAllCats.isChecked
outState.putBoolean(CHECKBOX_KEY, isAllCatsChecked)
outState.putInt(CATEGORY_KEY, i)
}
}
Now on every screen rotation, the fragment gets recreated twice, first with savedInstanceState != null and then with savedInstanceState == null, which means that I don't have access to the old settings in the Fragments. As I know, it is because the fragment gets recreated, and then the MainFragment containing the viewpager gets also recreated, which means that the fragments within the viewPager get created a second time. I have tried something like this in the MainFragment:
private lateinit var fragmentStatsCat: StatsCategoryFragment
private lateinit var fragmentStatsMon: StatsMonthFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState == null) {
fragmentStatsMon = StatsMonthFragment()
fragmentStatsCat = StatsCategoryFragment()
} else {
//Finding old fragments
}
}
Maybe this would solve the problem (I am not sure), but I don't know how I can find the fragments, as they are inside a ViewPager.
EDIT
I have now found a solution to find the fragments when the activity is recreated:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState == null)
{
fragmentStatsMon = StatsMonthFragment()
fragmentStatsCat = StatsCategoryFragment()
} else {
fragmentStatsMon = childFragmentManager.getFragment(savedInstanceState, FRAG_MONTH_TAG) as StatsMonthFragment
fragmentStatsCat = childFragmentManager.getFragment(savedInstanceState, FRAG_CAT_TAG) as StatsCategoryFragment
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
childFragmentManager.putFragment(outState, FRAG_CAT_TAG, fragmentStatsCat)
childFragmentManager.putFragment(outState, FRAG_MONTH_TAG, fragmentStatsMon)
}
This puts a reference in the Bundle with which I can get the fragment afterwards. But now, I have another problem. When the activity is recreated, it throws the following exception:
java.lang.IllegalStateException: Fragment already added: StatsCategoryFragment{f06ec65 (a1991f39-3af4-4a74-9aeb-79274beae04a) id=0x7f09014a}
I don't know how to solve that because I don't really manually add the fragments. Also, I don't really get it because the viewpager and the adapter also get recreated, don't they? So why are the fragments still attached to the adapter?
I also don't know in which line of code this happens, I could neither find it out in the logcat nor by debugging.
Put this line in AndroidManifest.xml in the entry of your Activity in which you are loading your Fragments:
android:configChanges="layoutDirection|keyboardHidden|orientation|screenSize"
By this, Data will not be reset when your Fragment is recreated by any condition.
Example:
<activity
android:name=".activity.MainActivity"
android:configChanges="layoutDirection|keyboardHidden|orientation|screenSize"
android:label="#string/app_name"></activity>
Check out this similar question
https://www.google.com/url?sa=t&source=web&rct=j&url=https://stackoverflow.com/questions/15313598/once-for-all-how-to-correctly-save-instance-state-of-fragments-in-back-stack&ved=2ahUKEwja6rasobzjAhVh1uAKHRakDXIQFjAAegQIBhAB&usg=AOvVaw0Hg_vwSnBayxV1EJfKOUZk

App crash after activity has been killed in background

i have an issue with an app that use ViewPager for display fragment. All works fine until the app goes in background and be killed from OS. It seems that after restore i have 2 IncidentScreenFragment that handle events, one with a null presenter (MVP) that crash my app.
My HomeActivity looks like:
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
}
}
}
And my Adapter:
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)
}
}
Each Fragment have a static method that return new Fragment:
companion object {
fun newInstance(): HomeScreenFragment {
return HomeScreenFragment()
}
}
When the app has been killed in background i figure out that there is 2 objects (Fragment) that listen to event, one with Presenter correctly instantiate and one without.
Below my abstract BaseFragment class:
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
Incident Fragment code:
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()
}
}
When i click on the button in homePage to display fragment content i got:
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)
If i try to print the id of Fragment, i obtain 2 different ids from method call showOnlyThisLevel() and onBackPressed(). What i miss ?
After doing some research, it seems that the problem stems from the misnaming of FragmentPagerAdapter's method - being named getItem(), but not clearly specifying that the abstract method getItem(int position) is supposed to return a new instance of a fragment rather than just "get an instance of one".
Of course, there is not much we can do about an incorrect name after it's been out in the wild for 7 years, but at least we can fix the bug that stems from this issue in your code ;)
Without further ado, the cause of your NPE is that onCreateView (where your Presenter is instantiated) is never called.
This happens because you are creating the fragment here:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
homeFragment = HomeScreenFragment.newInstance()
incidentFragment = IncidentScreenFragment.newInstance()
}
You return this fragment from inside getItem(int position) in your FragmentPagerAdapter:
override fun getItem(position: Int): Fragment = when(position) {
...
1 -> activity.incidentFragment
...
}
So what we know about activity.incidentFragment is that in it, onCreateView() is never called.
This is caused by the fact that it's never actually added to a FragmentManager and never displayed on the screen.
That's because super.onCreate(savedInstanceState) in Activity recreates all Fragments, using their no-args constructor, via reflection, while keeping their tag (see findFragmentByTag).
So as you can see in this answer, or as I can quote here:
// 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));
The getItem(position) method is only called if the Fragment is not found by the fragment tag that the FragmentPagerAdapter sets for the fragment, which IS automatically recreated after low memory condition kills your app.
Therefore, YOUR new fragment (that you create by hand in the Activity) is NEVER used, and therefore it has no view, never initialized, never added to FragmentManager, it's not the same instance as what's actually inside your ViewPager, and it crashes when you call it. Boom!
Solution is to instantiate the Fragment inside FragmentPagerAdapter's getItem(position) method. To get an instance of the fragment, use this answer.

Categories

Resources