I'm currently developing an android app and I've encountered a trouble that I'm not able to resolve myself.
Introduction
I've created a new project and picked "Tabbed Activity" as a template for the project. As you may know, the project created with this template, has 2 .xml files: activity_main.xml (that contains AppBarLayout and ViewPager2) and fragment_main.xml. It also has MainActivity.kt set up to make tabs work, and ui.main package with 3 .kt files in it that are responsible for displaying tabs content e.g. "Fragment #1" etc.
What do I want to have
I need the application to have top action bar with title, logo and tabs navigation. In total, I need to have 3 different tabs (fragments) with their own layout and logic.
What did I do and what happend
So, I've customized the activity_main.xml layout, then created a new layout fragment_dashboard.xml for the one of the fragments that I want to have in the application.
I've deleted auto generated code and wrote my own. Since I'm mostly like as a beginner in android development, I've used google to learn how to bind tabs, fragments and main activity together. I found several articles that I considered suitable for me.
After I finished the code, I wanted to check how my customized action bar with tabs and the half-finished fragment_dashboard.xml layout look together.
So I tried to run the app and encoutered the problem: when app starts in the emulated phone there is just a white screen and nothing else... (before deleting the auto-generated code for tabs it run without any problems)
What I tried to do
First of all I tried to debug MainActivity.kt. I put a breakpoint at the first line of the function onCreate():
super.onCreate(savedInstanceState, persistentState)
But when I run app in debug mode, debuger does not stop at this breakpoint. Thus, I came to a conclusion: execution does not even get to the function onCreate().
So, the question is: what am I doing wrong and how can I fix it to be able to see tabs and their fragments?
Code
DashboardFragment.kt
class DashboardFragment() : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_dashboard, container, false);
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.i("DashboardFragment","onViewCreated")
super.onViewCreated(view, savedInstanceState)
}
}
ViewPagerFragmentStateAdapter.kt
class ViewPagerFragmentStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
var positionToPageName = mapOf(
0 to "Dashboard"
)
private var _pageNameToFragment = mapOf<String, Fragment>(
"Dashboard" to DashboardFragment()
)
override fun getItemCount(): Int = _pageNameToFragment.size
override fun createFragment(position: Int): Fragment {
val pageName = positionToPageName[position]
val page = _pageNameToFragment[pageName]
return page ?: DashboardFragment() as Fragment
}
}
ViewPagerFragmentStateAdapter.kt
class MainActivity : AppCompatActivity() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager: ViewPager2
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
setContentView(R.layout.activity_main)
val adapter = ViewPagerFragmentStateAdapter(this)
viewPager = findViewById(R.id.view_pager)
viewPager.adapter = adapter
tabLayout = findViewById(R.id.tabs)
TabLayoutMediator(tabLayout, viewPager) {
tab, position -> tab.text = adapter.positionToPageName[position]
}.attach()
}
}
I've tried to change some things in the MainActivity.kt. It seems that the problem was just in the definition of the onCreate() function:
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
I've created a test project with "Tabbed Activity" template again and noticed that in the new project the persistentState argument is missing. So, I just removed it from definition in my project and the project started working well.
Now the definition of the onCreate() looks like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Related
I'm relatively new to Android / Kotlin.
I wonder how Android libraries like AdMob manage to create and show a View (especially an Intersitial Ad) from inside a library without any layout preparation of the integrating app. I assume this View is some sort of Fragment.
Sample code to show an intersitial ad from AdMob:
I think it somehow has to do with the Activity passed as a parameter in the show method.
if (mInterstitialAd != null) {
mInterstitialAd?.show(this)
} else {
Log.d("TAG", "The interstitial ad wasn't ready yet.")
}
This Guide states, that to add a fragment programmatically, "the layout should include a FragmentContainerView". Additionally in the sample code from the same guide the id of said FragmentContainerView is used to add the fragment. This id is not known inside the library.
class ExampleActivity : AppCompatActivity(R.layout.example_activity) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<ExampleFragment>(R.id.fragment_container_view)
}
}
}
}
How does such a library achieve this?
I wonder how Android libraries like AdMob manage to create and show a View (especially an Intersitial Ad) from inside a library without any layout preparation of the integrating app.
What "layout preparations" would the integrating app need to do? Given an Activity, which you pass to the method, any code can use Activity.startActivity to launch it's own Activity which can be styled / themed in any way with any layout the library chooses (such as showing an interstitial ad).
I assume this View is some sort of Fragment.
Why would you assume that? It could be a Fragment, but it would be contained within an Activity, which could be launched as I've indicated above.
This Guide states, that to add a fragment programmatically, "the layout should include a FragmentContainerView". Additionally in the sample code from the same guide the id of said FragmentContainerView is used to add the fragment. This id is not known inside the library.
Right. But that again assumes tha the library is only using a Fragment and trying to shove it into your heirarchy. That's highly unlikely. It's more likely starting a brand new Activity that it knows about and has full control over.
I managed to achieve it.
A working demo can be found here: https://github.com/eat-your-broccoli/add-fragment-from-library-demo
Library:
class FragmentManager {
companion object {
fun showFragment(activity: AppCompatActivity) {
(activity.supportFragmentManager.findFragmentById(android.R.id.content) == null) {
activity.supportFragmentManager.beginTransaction()
.add(android.R.id.content, DemoFragment())
.addToBackStack(null)
.commit()
}
}
}
}
class DemoFragment : Fragment() {
private lateinit var btnBack: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var view = inflater.inflate(R.layout.fragment_demo, container, false)
// enable back button
btnBack = view.findViewById(R.id.btn_back)
btnBack.setOnClickListener {
this.activity?.supportFragmentManager?.popBackStack()
}
return view
}
}
And in my activity:
class MainActivity : AppCompatActivity() {
lateinit var btnSwitch: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnSwitch = findViewById(R.id.btn_switch)
btnSwitch.setOnClickListener {
FragmentManager.showFragment(this)
}
}
}
A problem I had was that using R.id.content instead of android.R.id.content caused an execption and crashed the app.
Example of what i'm trying to achieve https://i.stack.imgur.com/lhp10.png
I'm stuck on the part where i need to switch to createUserFragment from defaultFragment
i attached this to my button, but nothing happens when i press it, not sure what's wrong there
MainActivity
class MainActivity : AppCompatActivity() {
lateinit var appBarConfiguration: AppBarConfiguration
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = findNavController(R.id.hostFragment)
appBarConfiguration = AppBarConfiguration(navController.graph,drawer_layout)
NavigationUI.setupActionBarWithNavController(this, navController,drawer_layout)
NavigationUI.setupWithNavController(navigation_drawer,navController)
}
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController,appBarConfiguration)
}
defaultFragment
class defaultFragment : Fragment() {
private lateinit var binding: defaultFragmentBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = defaultFragmentBinding.inflate(inflater)
return (binding.root)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.createNewBlankFAB.setOnClickListener{
val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
transaction.replace(R.id.defaultFragment, createUserFragment())
transaction.disallowAddToBackStack()
transaction.commit()
}
}
}
My questions are:
1)How can i fix my thing?
2)Will it work properly? I.e no memory leaks or some other funky stuff
3)Do i have to use another fragment for data input or maybe there's another way?
UPDATE
I stil have no idea how this works, but apparently i was replacing wrong fragment, i switched this R.id.defaultFragment in transaction.replace to R.id.hostFragment from which, i assume, all other fragments spawn, but now it just spawn on top of my existing fragment and the drawer button doesn't change its state, i guess i have to either do all of this differently or somehow pass to the drawer navigation information that current fragment was changed?
This is how we navigate within fragments in Navigation component library . We use navigate to then id of the destination Fragment which is defined in navgraph
binding.createNewBlankFAB.setOnClickListener{
Navigation.findNavController(view).navigate(R.id.createUserFragment);
}
I think you have to call "transaction.add" instead of "replace" since you are calling your fragment from an activity. Replace is called when you call a fragment from another fragment, I believe.
Dear StackOverflow Community!
My question might be a rookie one, I feel like I'm missing something very basic. I tried to make an ArcGIS map work under an android ViewPager2 structure. The map diplays nicely but when I navigate away to another fragment in the view pager, then back to the map, the app crashes with the following exception.
com.esri.arcgisruntime.ArcGISRuntimeException: vector:
/home/jenkins/100.7.0/dev_android_java_RTCA_release/runtimecore/c_api/src/mapping/map_view/geo_view.cpp(701) : error : Exception caught in __FUNCTION__
at com.esri.arcgisruntime.internal.jni.CoreGeoView.nativeDraw(Native Method)
at com.esri.arcgisruntime.internal.jni.CoreGeoView.a(SourceFile:346)
at com.esri.arcgisruntime.internal.h.b.o.a(SourceFile:132)
at com.esri.arcgisruntime.mapping.view.MapView.onDrawFrame(SourceFile:156)
at com.esri.arcgisruntime.mapping.view.GeoView$b.onDrawFrame(SourceFile:1363)
at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1573)
at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1272)
This seems to happen every time when the onResume() method of the Fragment containing the MapView is called. In this function (ie. the onResume()) I manually call onResume() on the MapView instance as indicated in this walkthrough:
https://developers.arcgis.com/labs/android/create-a-starter-app/
I extracted the problematic part of the code to a test app, I removed all layers, now it's just an empty basemap in an empty app (under the view pager structure) and the crash persists.
The reason why I think this problem could be connected with the ViewPager2 is because in a previous version of the app, I used a different navigation structure without the view pager and the map was working fine.
The difference between my actual code and the above walkthrough is that it puts the MapView directly under the MainActivity while I put it in a fragment as I'm working with a view pager.
It was not absolutely clear to me if I still have to put the appropriate onPause(), onResume(), onDestroy() calls under the fragment class or under the main activity so I tried both (you can see the former in the code below) and I also tried removing those override functions completely. The exception was the same in each case.
Here is the test app MainActivity class building the view pager.
class MainActivity : AppCompatActivity() {
private lateinit var adapter: ViewPagerFragmentAdapter
private lateinit var viewPager: ViewPager2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter = ViewPagerFragmentAdapter(supportFragmentManager, lifecycle)
viewPager = findViewById(R.id.view_pager)
viewPager.adapter = adapter
}
}
And the MapFragment class.
class MapFragment: Fragment() {
lateinit var mMapView: MapView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_map, container, false)
ArcGISRuntimeEnvironment.setLicense(resources.getString(R.string.arcgis_license_key))
mMapView = view.findViewById(R.id.map_view)
val basemapType = Basemap.Type.IMAGERY_WITH_LABELS
val latitude = 48.0166175
val longitude = 19.0339708
val levelOfDetail = 2
mMapView.map = ArcGISMap(basemapType, latitude, longitude, levelOfDetail)
return view
}
override fun onPause() {
if (mMapView != null) {
mMapView.pause()
}
super.onPause()
}
override fun onResume() {
super.onResume()
if (mMapView != null) {
mMapView.resume()
}
}
override fun onDestroy() {
if (mMapView != null) {
mMapView.dispose()
}
super.onDestroy()
}
}
Could you please give me any indicaton on where I could go wrong?
Thank you very much for any help in advance!
Mark
For anyone bumping into problems like this, always check your dependencies first :)
In my case, the ESRI lib was outdated. When I switched from the outdated dependency:
dependencies {
...
implementation 'com.esri.arcgisruntime:arcgis-android:100.7.0'
...
}
to the latest one:
dependencies {
...
implementation 'com.esri.arcgisruntime:arcgis-android:100.8.0'
...
}
the exception disappeared and the app works as expected.
edit: 2020.4.12 correct typo from Button.performClick() to button.performClick()
I am writing an app which should display a splash page/fragment for a
few seconds at start then display the next fragment in the navgraph. There are seven fragments in the navgraph which I can navigate around those fragments just fine.
The issue is with the splash fragment, I can only get the splash fragment to display/inflate when the button.onClickListener is set to accept a manual user
input -> click. (vs using button.performClick())
The desired end result is to display a fragment layout consisting of an image view and a text view for a few seconds at app start before displaying the next fragment layout in the navgraph, without having the user to click or press anything.
I have tried using threadsleep, a runnable with a handler, and even a while loop with performClick(). None of which have yielded acceptable results. The closest I have come to getting the desired result is the following:
class SplashFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
updateABode()
}
// pizzaLoop initialized to give about 4 seconds delay
val pizzaLoop = 1500000000
while (pizzaLoop > 0) {
pizzaLoop--
if (pizzaLoop == 0) {
button.performClick()
}
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}
With the pizzaLoop installed, the splash fragment will not inflate, but I do see the delay via the firmware screen update. (intially all I get is a white blank screen then subsequent calls to the SplashFragment class show nothing but the firmwareFragment screen (next in the navgraph) -- and the pizzaLoop delay is noticable).
When I comment out the pizzaLoop then the splash fragment displays as intended but I have to click the button to bring up the next fragment in the navgraph (the rest of the navgraph works fine).
It's like the button.performClick() method is preventing the inflation of the splash fragment.
EDIT: 2020.4.12 TO PROPERLY POST SOLUTION.
class SplashFragment : Fragment() {
private val handler: Handler = Handler()
private val updateRunnable: Runnable = Runnable { updateABode() }
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
handler.removeCallbacks(updateRunnable)
}
handler.postDelayed(updateRunnable, 4000)
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}
This question already has an answer here:
Android - Manage layouts landscape
(1 answer)
Closed 4 years ago.
I'm facing this problem from a week without success.
I'm trying to load two different fragments: PortraitTestFrag.java and LandscapeTestFrag.java depending on Activity host orientation.
Theese Fragments are loaded inside /layout/activity_main.xml and /layout-land/activity_main.xml like this:
<fragment
android:id="#+id/navigationContainerFragment"
android:name="class name#"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
What I want is when my Activity host is portrait, PortraitTestFrag is loaded and show. When my Activity is landscape then LandscapeTestFrag should load and show.
Problem is that PortraitTestFrag is visible at the startup but when device is rotating LandscapeTestFrag is never show even Activity is destroyed and recreated. It seem the first loaded Fragment has the priority.
What could be the problem?
I don't recommend you to replace fragment on orientation changes at least because you will loose your data saved to bundle or persisted inside ViewModel/Presenter/etc.
It is probably better to use DI or fabrics to change implementation of orientation-specific logic inside fragment.
If you really want to change whole fragment, you can create a proxy fragment which manages switch logic:
abstract class BaseSwitchFragment : Fragment() {
companion object {
private const val KEY_ORIENTATION = "ORIENTATION"
private const val CHILD_TAG = "CHILD_TAG"
}
private var prevOrientation: Int? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_switch, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
prevOrientation = savedInstanceState?.getInt(BaseSwitchFragment.KEY_ORIENTATION)
if (prevOrientation != resources.configuration.orientation) {
childFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, buildFragment(), CHILD_TAG)
.commit()
}
prevOrientation = resources.configuration.orientation
super.onViewCreated(view, savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_ORIENTATION, resources.configuration.orientation)
super.onSaveInstanceState(outState)
}
private fun buildFragment(): Fragment {
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
return buildPortraitFragment()
} else {
return buildLandscapeFragment()
}
}
protected abstract fun buildPortraitFragment(): Fragment
protected abstract fun buildLandscapeFragment(): Fragment
}
I have not tested this code, but probably it should work. Also I believe that it is possible to optimize this code to prevent recreation of child fragment if it is going to be removed.