So basically I have an app where I move across different fragments by using NavController navigation between fragments. This is working on all Android versions < 13.
findNavController().navigate(R.id.step02Fragment)
The issue on Android 13 is that when I move from the Step05Fragment to the Step06Fragment the onPause and onResume methods of the Step05Fragment start to execute forever and ever, causing a StackOverflow error.
I have no idea why this is happening between these two steps and in this specific Android version. All other fragments do the navigation the exact same way also and this issue doesn't happen on them.
Any ideas?
Thanks
P.D These are the libraries versions, if this is a known issue on any of them
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-service:2.5.1"
implementation "androidx.navigation:navigation-runtime-ktx:2.5.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.navigation:navigation-ui-ktx:2.5.2"
implementation "androidx.navigation:navigation-dynamic-features-fragment:2.5.2"
Here is the code of the fragments
#AndroidEntryPoint
class Step05Fragment : Fragment() {
lateinit var binding: FragmentStep05Binding
private var initialSetup: Boolean = true
private val disposable = CompositeDisposable()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStep05Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
initialSetup = it
}
setObservables()
setView()
}
override fun onDestroy() {
super.onDestroy()
disposable.dispose()
}
private fun setObservables() {
binding.btnEnable.clicks()
.map {
checkPermission()
}
.subscribe()
.disposeBy(disposable)
}
private fun setView() {
if (!initialSetup) {
binding.currentStep.visibility = View.GONE
}
}
private fun checkPermission() {
if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
handleNavigation()
}
}
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
handleNavigation()
} else {
checkPermission()
}
}
private fun handleNavigation() {
if (initialSetup) {
findNavController().navigate(R.id.step06Fragment)
} else {
activity?.finishAffinity()
}
}
}
#AndroidEntryPoint
class Step06Fragment : Fragment() {
lateinit var binding: FragmentStep06Binding
private var initialSetup: Boolean = true
private val disposable = CompositeDisposable()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStep06Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
initialSetup = it
}
setObservables()
setView()
}
override fun onDestroy() {
super.onDestroy()
disposable.dispose()
}
private fun setObservables() {
binding.btnEnable.clicks()
.map {
checkPermission()
}
.subscribe()
.disposeBy(disposable)
}
private fun setView() {
if (!initialSetup) {
binding.currentStep.visibility = View.GONE
}
}
private fun checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!PermissionsUtils.isMediaLocationPermissionGranted(requireContext())) {
requestPermission.launch(Manifest.permission.ACCESS_MEDIA_LOCATION)
} else {
handleNavigation()
}
} else {
handleNavigation()
}
}
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
handleNavigation()
} else {
checkPermission()
}
}
private fun handleNavigation() {
if (initialSetup) {
findNavController().navigate(R.id.step07Fragment)
} else {
activity?.finishAffinity()
}
}
After a while, found the answer
https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
Seems like this weird behavior is because of this, added the following validation after updating my project dependencies to aim Android 13 and it worked just fine
private fun checkPermission() {
if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermission.launch(Manifest.permission.READ_MEDIA_IMAGES)
} else {
requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
handleNavigation()
}
}
this can be related to binding! I had some binding issues before. Can you try changing it to the old findViewById or another way?
Related
I am implementing a function which is to close the application by clicking back button twice. It shows the toast message when it is first clicked. However, the problem is it shows infinite loop error then is termiated. I do not really understand the reason after two days of browsing. Any help will be greatly appreciated.
My fragment code
class RegisteredMainFragment : Fragment() {
private lateinit var binding : FragmentRegisteredMainBinding
private val viewModel : RegisteredMainViewModel by inject()
private lateinit var mContext: MainActivity
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context as MainActivity
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_registered_main, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
onBackPressedCallback()
}
private fun onBackPressedCallback() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,
object : OnBackPressedCallback(true ) {
override fun handleOnBackPressed() {
if (doubleBackToExitPressedOnce) {
requireFragmentManager().popBackStack()
return
}
doubleBackToExitPressedOnce = true
Toast.makeText(requireContext(), "click back button again", Toast.LENGTH_SHORT).show()
Handler().postDelayed({ doubleBackToExitPressedOnce = false }, 2000)
}
})
}
override fun onResume() {
super.onResume()
viewModel.onAuthExist()
}
override fun onDestroyView() {
super.onDestroyView()
PushObserverService.unregisterObserver(this)
}
}
I solved the problem by writing it in override fun onAttach
Please check the code below
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context as MainActivity
callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (doubleBackToExitPressedOnce) {
activity?.finish()
}
doubleBackToExitPressedOnce = true
Handler().postDelayed({ doubleBackToExitPressedOnce = false }, 2000)
showSnackBar(
context = mContext,
layout = binding.layoutMain,
type = Constants.SnackBarTypes.Warn,
message = mContext.getString(R.string.tap_twice_to_terminate)
)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
I've been trying to follow the only good example with support that I could find, but in my case, it doesn't work.
I have a ViewModel that talks to a #Model in Composable, and changes a loading: Bool according to a MutableLiveData<Boolean> but it doesn't recompose.
class LoaderViewModel : ViewModel() {
val loadingLiveData = MutableLiveData<Boolean>(false)
fun fetch() {
viewModelScope.launch {
val flow = flowOf("result")
.onStart {
loadingLiveData.value = true
delay(2000)
}
.onCompletion {
loadingLiveData.value = false
}
.collect {
// Do something with the result
}
}
}
}
class LoaderFragment : Fragment() {
private val viewModel: LoaderViewModel by viewModel()
#Model
class ActivityLoadingState(var loading: Boolean = false)
private val activityLoadingState = ActivityLoadingState()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FrameLayout(context ?: return null).apply {
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setContent {
Loader()
}
}
}
#Composable
fun Loader() = MaterialTheme {
val loadingModel = activityLoadingState
Container {
Center {
if (loadingModel.loading) {
CircularProgressIndicator(
color = Color(0xFFFF0000)
)
} else {
Container { }
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeUI()
viewModel.fetch()
}
private fun subscribeUI() {
viewModel.loadingLiveData.observe(viewLifecycleOwner) {
activityLoadingState.loading = it
}
}
}
What I am doing, is to have Flows in my ViewModel, and use function collectAsState() whitin composables.
I'm trying to get my location using mapbox on an android application built with kotlin. I'm using the locationComponent method to get it, here is my code :
class PlaceholderFragment : Fragment(), OnMapReadyCallback, PermissionsListener {
private var permissionsManager: PermissionsManager = PermissionsManager(this)
private var mapboxMap: MapboxMap? = null
private var myCurrentLocation: LatLng? = null
var navigationMapRoute: NavigationMapRoute? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageViewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
setIndex(arguments?.getInt(ARG_SECTION_NUMBER) ?: 1)
}
val theActivity = activity as NavigationActivity?
theSteps = theActivity?.theSteps
case = theActivity?.case
case = theActivity.case
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.let {
Mapbox.getInstance(
it,
getString(com.innoventiq.arkbeh.R.string.mapbox_access_token2)
)
}
val root =
inflater.inflate(com.innoventiq.arkbeh.R.layout.fragment_navigation, container, false)
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)
getMyLocation()
}
override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) {
Toast.makeText(
activity,
com.innoventiq.arkbeh.R.string.user_location_permission_explanation,
Toast.LENGTH_LONG
)
.show()
}
override fun onPermissionResult(granted: Boolean) {
if (granted) {
enableLocationComponent(mapboxMap?.style!!)
} else {
Toast.makeText(
activity,
com.innoventiq.arkbeh.R.string.user_location_permission_not_granted,
Toast.LENGTH_LONG
)
.show()
}
}
override fun onMapReady(mapboxMap: MapboxMap) {
this.mapboxMap = mapboxMap
mapboxMap.cameraPosition = CameraPosition.Builder()
.target(myCurrentLocation)
.zoom(14.0)
.build()
//mapView.setOnTouchListener { v, event -> true }
mapboxMap.setStyle(Style.OUTDOORS) {
enableLocationComponent(it)
}
}
#SuppressLint("MissingPermission")
private fun enableLocationComponent(loadedMapStyle: Style) {
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(activity)) {
// Create and customize the LocationComponent's options
val customLocationComponentOptions = activity?.let {
LocationComponentOptions.builder(it)
.trackingGesturesManagement(true)
.accuracyColor(
ContextCompat.getColor(
activity!!,
com.innoventiq.arkbeh.R.color.colorGreen
)
)
.build()
}
val locationComponentActivationOptions =
activity?.let {
LocationComponentActivationOptions.builder(it, loadedMapStyle)
.locationComponentOptions(customLocationComponentOptions)
.build()
}
// Get an instance of the LocationComponent and then adjust its settings
mapboxMap?.locationComponent?.apply {
// Activate the LocationComponent with options
locationComponentActivationOptions?.let {
this?.activateLocationComponent(
it
)
}
// Enable to make the LocationComponent visible
isLocationComponentEnabled = true
// Set the LocationComponent's camera mode
cameraMode = CameraMode.TRACKING
// Set the LocationComponent's render mode
renderMode = RenderMode.COMPASS
}
} else {
permissionsManager = PermissionsManager(this)
permissionsManager.requestLocationPermissions(activity)
}
}
private fun getMyLocation() {
myCurrentLocation =
mapboxMap?.locationComponent?.lastKnownLocation?.latitude?.let {
mapboxMap!!.locationComponent.lastKnownLocation?.longitude?.let { it1 ->
LatLng(it,
it1
)
}
}
println("the location : $myCurrentLocation ")
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
companion object {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private const val ARG_SECTION_NUMBER = "section_number"
/**
* Returns a new instance of this fragment for the given section
* number.
*/
#JvmStatic
fun newInstance(sectionNumber: Int): PlaceholderFragment {
return PlaceholderFragment().apply {
arguments = Bundle().apply {
putInt(ARG_SECTION_NUMBER, sectionNumber)
}
}
}
}
}
I used almost the full fragment code just trying to give you a clear view of the steps I've used to get the location. When it comes to the line println("the location : $myCurrentLocation ")
inside the getMyLocation() function , it returns this output the location : null ,, any help on this ?
Note
When the map loads, it shows my location perfectly and tracks it, but I just can't get the LatLng of it.
I've found the answer, used LocationComponent.LocationEngine.getLastLocation callBack to get it , here is the code:
private fun getMyLocation() {
mapboxMap?.locationComponent?.locationEngine?.getLastLocation(object :
LocationEngineCallback<LocationEngineResult> {
override fun onSuccess(result: LocationEngineResult?) {
if (result != null) {
myCurrentLocation =
LatLng(result.locations[0].latitude, result.locations[0].longitude)
println("my location is : $myCurrentLocation")
getTheRoute(myCurrentLocation)
}
}
override fun onFailure(exception: Exception) {
toast(getString(R.string.failed_get_location))
}
})
}
I want develop one application with Rxjava, Dagger, Kotin and MVP.
I write below codes but when run application show me nullPointerExecetpion error.
I know nullPointerExeception for my codes bug, but i try to found my bug i can't it!
My base fragment codes:
abstract class BaseFragment : Fragment(), BaseView {
var presenter: BasePresenter<*>? = null
abstract fun initializeDagger()
abstract fun initializePresenter()
abstract var layoutID: Int
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(layoutID, container, false)
initializeDagger()
initializePresenter()
return view
}
override fun onDetach() {
super.onDetach()
presenter?.onDestroy()
}
}
HomePresenter code:
class HomeTodayPresenter #Inject constructor(
val repositoryUseCase: RepositoryUseCase, disposable: CompositeDisposable
) : BasePresenter<HomeTodayView>(disposable) {
private var todayList = ArrayList<Today>()
fun onCreate() {
view?.initRepositoryList(todayList)
getTodayList()
}
fun onTodayRefreshList() {
getTodayList()
}
private fun getTodayList() {
compositeDisposable.add(
repositoryUseCase.getAuctionsToday()
.subscribe({ responseResult ->
view?.hideLoader()
responseResult?.let { itResponse ->
itResponse.res?.let { itRes ->
itRes.today?.let { itToday ->
if (itToday.size > 0) {
todayList.clear()
todayList.addAll(itToday)
view?.loadRepositoryList()
}
}
}
}
}, { e ->
view?.let { itView ->
e.message?.let { itErr ->
itView.showErrorMessage(itErr)
}
itView.hideLoader()
}
})
)
}
}
Home Fragment code:
class HomeTodayFragment : BaseFragment(), HomeTodayView {
#Inject
lateinit var homeTodayPresenter: HomeTodayPresenter
lateinit var todayAuctionsAdapter: TodayAuctionsAdapter
lateinit var layoutManager: LinearLayoutManager
private val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener {
homeTodayPresenter.onTodayRefreshList()
}
override fun initRepositoryList(list: ArrayList<Today>) {
layoutManager = LinearLayoutManager(requireContext())
todayAuctionsAdapter = TodayAuctionsAdapter(list)
requireContext().initRecyclerView(homeFragToday_list, layoutManager, todayAuctionsAdapter)
}
override fun loadRepositoryList() {
todayAuctionsAdapter.notifyDataSetChanged()
}
override fun hideLoader() {
homeFragToday_loader.visibility = View.GONE
}
override fun showErrorMessage(msg: String) {
Log.e("responseErr", msg)
}
override fun initializeDagger() {
AndroidInjection.inject(requireActivity())
}
override fun initializePresenter() {
super.presenter = homeTodayPresenter
homeTodayPresenter.view = this
}
override var layoutID: Int = R.layout.fragment_home_today
private lateinit var toolbarTile: TextView
lateinit var handler: Handler
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
homeTodayPresenter.onCreate()
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
//Initialize
handler = Handler()
//Set delay
handler.postDelayed({
//Set title
toolbarTile.text = resources.getString(R.string.today)
toolbarTile.setTextColor(ResourcesCompat.getColor(resources, R.color.green_active, null))
}, 10)
}
}
}
LogCat error :
kotlin.UninitializedPropertyAccessException: lateinit property homeTodayPresenter has not been initialized
at com.app.applisttestapp.UI.Home.Fragments.Today.HomeTodayFragment.onCreateView(HomeTodayFragment.kt:69)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2539)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:875)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1227)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1293)
Show me error for this line : homeTodayPresenter.onCreate()
How can i fix it?
While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView.
The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.
AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter
Fragment
class GradeFragment : BaseFragment(), GradeView {
#Inject
lateinit var presenter: GradePresenter
private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
companion object {
fun newInstance() = GradeFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this#GradeFragment)
loadData()
}
}
override fun initView() {
gradeAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnUpdateListener { presenter.onUpdateDataList(it) }
setOnItemClickListener { position ->
getItem(position).let {
if (it is GradeItem) {
GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
}
}
}
}
gradeRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeAdapter
}
gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}
override fun updateData(data: List<GradeHeader>) {
gradeAdapter.updateDataSet(data, true)
}
override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty
override fun showEmptyView(show: Boolean) {
gradeEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else GONE
}
override fun setRefresh(show: Boolean) {
gradeSwipe.isRefreshing = show
}
Presenter
class GradePresenter #Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
override fun attachView(view: GradeView) {
super.attachView(view)
view.initView()
}
fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradeRepository.getGrades(it, forceRefresh) }
.map { it.groupBy { grade -> grade.subject } }
.map { createGradeItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally { view?.setRefresh(false) }
.doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
.doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
return items.map {
val gradesAverage = calcAverage(it.value)
GradeHeader().apply {
subject = it.key
average = view?.run {
if (gradesAverage == 0f) emptyAverageString()
else averageString().format(gradesAverage)
}.orEmpty()
number = view?.gradeNumberString(it.value.size).orEmpty()
subItems = (it.value.map { item ->
GradeItem().apply {
grade = item
weightString = view?.weightString().orEmpty()
valueColor = getValueColor(item.value)
}
})
}
}
}
fun onUpdateDataList(size: Int) {
if (size != 0) view?.showProgress(false)
}
After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating