I am using Kotlin Android Extension to access view directly by their id.
I have a progress bar which I access directly in fragment using id i.e progress_bar
<ProgressBar
android:id="#+id/progress_bar"
style="#style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="15dp"
android:indeterminate="true"/>
In fragment, I am showing and hiding it with this code
progress_bar.visibility = if (visible) View.VISIBLE else View.GONE
It is working perfectly until I rotate the screen. After that, it throws the exception
java.lang.IllegalStateException: progress_bar must not be null.
The variable gets null on screen rotation. How to solve this problem?
Fragment code
class SingleAppFragment : Fragment() {
private lateinit var appName: String
companion object {
fun newInstance(appName: String = ""): SingleAppFragment {
val fragment = SingleAppFragment()
val args = Bundle()
args.putString(Constants.EXTRA_APP_NAME, appName)
fragment.arguments = args
return fragment
}
}
private var mListener: OnFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appName = if (arguments != null && !arguments.getString(Constants.EXTRA_APP_NAME).isEmpty()) {
arguments.getString(Constants.EXTRA_APP_NAME)
} else {
Constants.APP_NAME_FACEBOOK
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater!!.inflate(R.layout.fragment_single_app, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
setEventListeners()
}
private fun initView() {
var canShowSnackBar = true
web_single_app.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
showHideProgressBar(true)
canShowSnackBar = true
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
showHideProgressBar(false)
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
web_single_app.stopLoading()
if (canShowSnackBar) {
mListener?.onErrorWebView()
canShowSnackBar = false
}
}
}
web_single_app.settings.javaScriptEnabled = true
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
private fun setEventListeners() {
back_web_control.setOnClickListener({
web_single_app.goBack()
})
}
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control.visibility = if (visible) View.VISIBLE else View.GONE
}
fun loadUrl(appName: String) {
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
mListener = context
}
}
override fun onDetach() {
super.onDetach()
mListener = null
}
interface OnFragmentInteractionListener {
fun onErrorWebView()
}
}
Steps to reproduce:
Start Activity
Fragment get loaded
At Fragment load, I load an URL and show a progress bar
At loading the URL I rotate the phone and the progress bar variable gets null
In my case this bug happens from time to time. Of course, onViewCreated() is a good method to place your code in. But sometimes it's strangely not enough. And setRetainInstance(true) may help, may not. So sometimes this helps: access your Views with a view variable. You can even access them inside onCreateView(). You can use ?. for a guarantee that an application won't crash (of course, some views won't update in this case). If you wish to get context, use view.context.
In my case this bug reproduced only in Kotlin coroutines.
private fun showProgress(view: View) {
view.progressBar?.visibility = View.VISIBLE
}
private fun hideProgress(view: View) {
view.progressBar?.visibility = View.GONE
}
Then in code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showData(view)
}
private fun showData(view: View) {
showProgress(view)
adapter = SomeAdapter()
adapter.setItems(items)
val divider = SomeItemDecoration(view.context)
view.recycler_view?.run {
addItemDecoration(divider)
adapter = this#SomeFragment.adapter
layoutManager = LinearLayoutManager(view.context)
setHasFixedSize(true)
}
hideProgress(view)
}
In which method do you get the progress_bar by Id?
Please consider the fragment state lifecycle. Maybe you try to load it when the view is not ready yet.
Ensure your progress_bar variable is assigned only after the view is ready. For example in the
onViewCreated method.
See here the official Android lifecycle:
Update
As #CoolMind pointed out the diagram doesn't show the method onViewCreated.
The complete Android Activity/Fragment lifecycle can be found here:
Add retain intance true to the fragment so that it will not be destroyed when an orientation changes occurs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance=true
}
Also do a null check using safe call operator before accessing views
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control?.visibility = if (visible) View.VISIBLE else View.GONE
}
Related
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.
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
Get next error from Crashlytics for Android version: 10:
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.content.Context.getSystemService(java.lang.String)' on a null object reference
at android.app.Dialog.<init>(Dialog.java:196)
at android.app.ColorBaseAlertDialog.<init>(ColorBaseAlertDialog.java:33)
at android.app.AlertDialog.<init>(AlertDialog.java:208)
at android.app.AlertDialog.<init>(AlertDialog.java:204)
at android.app.ProgressDialog.<init>(ProgressDialog.java:112)
at com.sau.authenticator.widget.fragment.BaseFragment.createProgressDialog(BaseFragment.java:46)
at com.sau.authenticator.widget.fragment.BaseFragment.showLoadProgress(BaseFragment.java:41)
at com.sau.authenticator.widget.fragment.WebViewFragment$webViewClient$1.onPageStarted(WebViewFragment.java:69)
at i6.c(i6.java:2)
at Hn.handleMessage(Hn.java:145)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:228)
at android.app.ActivityThread.main(ActivityThread.java:7782)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:981)
In my function createProgressDialog, which is called in BaseFragment:
abstract class BaseFragment : Fragment() {
private var progressDialog: ProgressDialog? = null
val activityComponents: ActivityComponentsContract?
get() = activity?.activityComponentsContract
override fun onDestroy() {
progressDialog?.dismiss()
progressDialog = null
super.onDestroy()
}
protected fun showLoadProgress() {
if (progressDialog == null) progressDialog = createProgressDialog()
progressDialog?.show()
}
private fun createProgressDialog(): ProgressDialog? {
val dialog = ProgressDialog(activity, R.style.ProgressDialogTheme)
dialog.setCancelable(false)
dialog.setProgressStyle(android.R.style.Widget_ProgressBar_Large)
return dialog
}
protected fun dismissLoadProgress() {
progressDialog?.dismiss()
}
}
Here is code of my WebViewFragment:
class WebViewFragment : BaseFragment() {
private var url = ""
private var title = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
url = it.getString(KEY_URL, "")
title = it.getString(KEY_TITLE, "")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
activityComponents?.updateAppbar(
title = title,
backActionImageResId = R.drawable.ic_appbar_action_back
)
return inflater.inflate(R.layout.fragment_web_view, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
customWebView?.webViewClient = webViewClient
customWebView?.loadUrl(url)
}
private val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
showLoadProgress()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
dismissLoadProgress()
}
}
companion object {
const val KEY_URL = "KEY_URL"
fun newBundle(url: String = "", title: String): Bundle {
return Bundle().apply {
putString(KEY_URL, url)
putString(KEY_TITLE, title)
}
}
}
}
I'm not too familiar with WebView, but it seems like by the time onPageStarted() is called your user already navigated away (or rotated his device). Hence your Fragment's onDestroyView() is likely called already, but you are not notifying your WebView that starts loading your url. That's why activity is null and you get the exception.
So on your WebViewFragment's lifecycle callbacks make sure you notify your WebView too:
override fun onResume() {
super.onResume()
customWebView?.onResume()
}
override fun onPause() {
super.onPause()
customWebView?.onPause()
}
override fun onDestroyView() {
super.onDestroyView()
customWebView?.stopLoading() // this should prevent onPageStarted() from being called
}
It seems that this error happened when your Fragment was DestroyView. I am not entirely sure about this. You can reproduce by load a large web page, then navigate to another fragment so that WebViewFragment / onViewDestroyed is executed. Print logs in onPageStarted and onPageFinished. If you see them being called when you destroy the view, this is exactly the cause. At this point you just need to check the lifecycle state to show or hide progress.
Need to paste context in my constructor in SwipeToDeleteCallback, which is located in the fragment:
private val swipeHandler = object : SwipeToDeleteCallback() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
presenter.removeListItem(viewHolder.adapterPosition)
}
}
Tried to insert requireContext() but catch KotlinNullPointerException.
Maybe I can somehow use the context here viewHolder.context?
Here is some code SwipeToDeleteCallback:
abstract class SwipeToDeleteCallback(context: Context) :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
private val deleteIcon = context.getDrawableCompat(R.drawable.ic_log_out)
Also i called my fragment from MainActivity:
override fun onPostResume() {
super.onPostResume()
replaceFragment(TargetsFragment())
}
override fun showListOfTarget() {
replaceFragment(TargetsFragment())
}
Here is full code of my fragment:
class TargetsFragment : Fragment(), ListItemClickListener, SelectTargetViewContract, BaseDataSetContract {
private var recyclerView: RecyclerView? = null
private val presenter = TargetsPresenter(this)
private var adapter = TargetsAdapter(clickListener = this)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_target_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.setInitialData()
setupViews()
updateListData()
}
override fun onListItemClick(itemIndex: Int, itemCode: String) {
presenter.onListItemClick((adapter.getItem(itemIndex) as Target).guid)
}
override fun showTarget(guid: String) {
activity?.addFragment(TargetEditFragment.newInstance(guid))
}
override fun dataSetChanged() {
updateListData()
adapter.notifyDataSetChanged()
}
override fun updateViewContent() {
adapter.data = presenter.targetList
recyclerView?.adapter = adapter
recyclerView?.setVisible(presenter.shouldShowContent())
emptyView?.setVisible(presenter.shouldShowEmptyView())
}
private fun updateListData() {
if (presenter.firebaseUser == null) {
Log.d("some", "loadLogInView")
} else {
presenter.getTargetsFromDb()
}
}
private val swipeHandler = object : SwipeToDeleteCallback() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
presenter.removeListItem(viewHolder.adapterPosition)
}
}
private fun setupViews() {
recyclerView = view?.findViewById(R.id.recyclerView)
recyclerView?.layoutManager = LinearLayoutManager(activity)
ItemTouchHelper(swipeHandler).attachToRecyclerView(recyclerView)
}
}
You can use
yourViewHolder.itemView.context
to get the context of yourViewHolder
After clarification from OP:
Initially I thought you only wanted to get context from your ViewHolder, but here's an addition to my answer:
Try moving your code which requires context to onAttach, which you will have to override, from the documentation : https://developer.android.com/guide/components/fragments.html
Caution: If you need a Context object within your Fragment, you can call getContext(). However, be careful to call getContext() only when the fragment is attached to an activity. When the fragment isn't attached yet, or was detached during the end of its lifecycle, getContext() returns null.
The method you're looking for looks like this :
override fun onAttach(context: Context) {
super.onAttach(context)
}
You will probably be safe to move all your code which requires context inside this method and assign it as you need to
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.