How to create and display Fragments from inside an Android Library? - android

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.

Related

Android Studio does not render layout

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)

How to handle buttons of custom Dialog using DialogFragment?

I have a custom dialog with X button, that dialog is called in a Fragment and here i have to manage all the clicks from the Alert.
Which is the best way to do so?
I actually was going to set the click listeners in the DialogFragment but i have to change some layout stuff and set variables from my Fragment so it will be better if i manage it from the fragment directly.
Here is my code now:
class ElencoDialog(private val testata: Testata, private val elimina: Boolean): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
}
}
}
And here is my fragment where i show the dialog:
class ElencoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false).show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true).show(parentFragmentManager, "ElencoDialog")
})
}
}
So instead of managing the click from the DialogFragment how can i manage the clicks directly from my Fragment?
First of all it is bad practice to have anything apart from the default constructor in a DialogFragment (or any Fragment). Although this might work initially, the system might need to recreate the fragment for various reasons, rotation, low memory etc. and it will attempt to use an empty constructor to do so. You should instead be using fragment arguments to pass simple data (covered in another question), a ViewModel for more complex data (I prefer this method anyway) or the new fragment results API, which I've outlined below.
But in answer to your specific question, to interact between your dialog fragment and main fragment, you have a few options:
Target fragment
You can set your original fragment as a target of your dialog but using setTargetFragment(Fragment). The fragment can then be retrieved safely from your dialog using getTargetFragment. It would probably be best practice to have your fragment implement an interface which you can cast to has the relevant callback methods.
Fragment results API
This is a relatively new API that attempts to replace the above, you can read more about it here: Communicating between fragments.
ViewModel
You can use a shared ViewModel scoped to the activity or parent fragment and keep your state in there. This would also solve the problem of having to pass your initial state through your fragment constructor. I won't explain how they work here as that's another question, but I would take a look here: ViewModel overview.
Pass callBack from fragment to your Dialog Fragment
class ElencoFragment() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
})
}
}
Your Dialog Fragment
class ElencoDialog(private val testata: Testata, private val elimina: Boolean, block : () -> Unit): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
block()
}
}
}

ArcGIS map crashes under android ViewPager2

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.

Fragment loses listener at orientation change

I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working

Android - Add different fragments depending on Activity host orientation [duplicate]

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.

Categories

Resources