Just to clarify beforehand, I have searched extensively in SO for similar question to find an answer to my question.
I will reference the following (to name a few sources) :
How to start a new activity from a non activity class in Android kotlin?
Start a new Activity from non Activity class
Basic Kotlin Tutorial
I have a MainActivity and four other different activities, let's call them A through D.
I also have a Utilities class which sets on click listeners to image buttons found in activities A through D.
These listeners then open a new activity, E.
For some reason, in the onCreate method for activity E, savedInstanceState is always null.
I have tried setting the listener from the MainActivity, but to no avail.
I have also passed the context from the MainActivity (instead of using the scroll view's), but that had not effect either
Below is a snippet of the code.
Utilities.kt
class Utilities {
companion object {
/...
fun setTooltipsAndListeners(scrollView: ScrollView) {
val buttons: ArrayList<View> = scrollView.touchables
for (button in buttons) {
val tooltipText = button.contentDescription
if (tooltipText != null) {
TooltipCompat.setTooltipText(button, tooltipText)
}
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
val tag: String? = v?.tag as? String
val intent = Intent(scrollView.context, ActivityE::class.java)
intent.putExtra("symbol_name", tooltipText)
intent.putExtra("symbol_image", tag)
scrollView.context.startActivity(intent)
}
})
}
}
/...
}
ActivityE.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_name)
setDataToUI(savedInstanceState)
}
private fun setDataToUI(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
Log.d("TAG", "savedInstanceState IS NULL")
return
}
/... Inner logic that is not relevant
}
Instead of savedInstanceState you need to use getIntent(). So your code will change like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_name)
setDataToUI(intent)
}
private fun setDataToUI(intent: Intent?) {
if (intent.getStringExtra("symbol_name") == null) {
Log.d("TAG", "No data passed for symbol_name")
return
}
// Do the same for other strings. Or alternatively, you can pass Bundle
// data from your Utility and retrieve the bundle through the intent as well
}
savedInstanceState is used to store data when the state of the activity is changed, but you are passing data through an Intent in your utility class
Related
This question already has answers here:
Passing data between a fragment and its container activity
(16 answers)
Closed 1 year ago.
How to correctly transfer data from a fragment to an activity? I do as follows:
1- Create an interface
interface IProfileToActivity {
fun profileInfo(data: AllHeroes.Global)
}
2- Then I inheritance in the activity
class ProfileActivity : AppCompatActivity(), IProfileToActivity {
private lateinit var myBinding: ActivityProfileBinding
override fun profileInfo(data: AllHeroes.Global) {
myBinding.tvUsername.text = data.name
myBinding.tvDivision.text = data.rank.rankName
Log.i("Apex Info 3", data.toString())
}
}
3- sending from a fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? IProfileToActivity)?.profileInfo(allInfoApexResponse.global)
mHeroesAdapter.heroesList(allAdapterListHero)
}
but nothing happens, why? what did I do wrong?
You need not create an interface here. You can use requireActivity() to get a reference to the parent activity. Using it you can access public fields and functions of you activity.
class ProfileActivity : AppCompatActivity() {
private lateinit var myBinding: ActivityProfileBinding
fun profileInfo(data: AllHeroes.Global) {
myBinding.tvUsername.text = data.name
myBinding.tvDivision.text = data.rank.rankName
Log.i("Apex Info 3", data.toString())
}
}
And in your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity as ProfileToActivity).profileInfo(allInfoApexResponse.global)
mHeroesAdapter.heroesList(allAdapterListHero)
}
There are many ways to pass data from fragment to activity:
Using shared ViewModel.
A ViewModel is used to manage and store UI related data in a
lifecycle conscious way.
~Read more
class SharedViewModel: ViewModel() {
private val currItems: MutableLiveData<List<Item>> =
MutableLiveData<List<Item>>(listOf())
fun getCurrItem(): LiveData<List<Item>> {
return currItems
}
fun sendCurrItems(items: MutableList<Item>) {
currItems.postValue(items)
}
}
class ItemFragment: Fragment() {
private val sharedModel: SharedViewModel by activityViewModels()
}
MainActivity: AppCompactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val model = ViewModelProvider(this).get(SharedViewModel::class.java)
}
}
In the above class, data is being stored and updated using an MutableList. Thing to be noted here is, the above class is a singleton class, i.e. once it is created, it gets destroyed only when the activity is ended.
Let us assume that an item has to be shared from a ItemFragment to the MainActivity
One callback has to be implemented the MainActivity. For that, one can use an Interface
interface ItemListener{
fun sendItem(item : MutableList<Item>)
}
ItemFragment:
class ItemFragment: Fragment() {
override fun sendItems(items: MutableList<Item>?) {
// Send an Item from here as well as update it
}
// Or just simply call sendItem method.
}
MainActivity:
class MainActivity: AppCompactActivity(){
fun receiveItem(context : Context){
private var mCallback: ItemListener? = null
mCallback = context
}
}
I haven't found anything over the past day that shows how to do this action, everything I've seen is with a basic button of which I am unable to replicate for use with an image button. using setOnClickListener does not seem to work at all though the only cases I found of using them were 5+ years old.
Is there a Storyboard equivalent of linking activities in Android Studio?
Here is an example I found but 7 years old.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myButton =
findViewById<View>(R.id.live) as ImageButton
myButton.setOnClickListener(object : OnClickListener() {
// When the button is pressed/clicked, it will run the code below
fun onClick() {
// Intent is what you use to start another activity
val intent = Intent(this, LiveActivity::class.java)
startActivity(intent)
}
})
}
}
gives the following error:
Object is not abstract and does not implement abstract member public abstract fun onClick(p0: View!): Unit defined in android.view.View.OnClickListener
The problem is that you haven't included the View parameter to your onClick override. The signature of OnClickListener.onClick includes a View (the View that was clicked) as its parameter, so onClick() (with no parameters) doesn't match that signature.
You could either add it explicitly (in which case you also need to refer to the Activity's this explicitly with ActivityName.this, as this refers to the OnClickListener otherwise):
myButton.setOnClickListener(object : View.OnClickListener {
// When the button is pressed/clicked, it will run the code below
override fun onClick(view: View) {
// Replace ActivityName with the name of your Activity that this is in
val intent = Intent(ActivityName.this, LiveActivity::class.java)
startActivity(intent)
}
})
or use Kotlin's SAM conversions to add it implicitly (I'd do this approach):
// When the button is pressed/clicked, it will run the code below
myButton.setOnClickListener { // there's an implicit view parameter as "it"
// Intent is what you use to start another activity
val intent = Intent(this, LiveActivity::class.java)
startActivity(intent)
}
Fix
Change from:
myButton.setOnClickListener(object : OnClickListener { })
to
myButton.setOnClickListener(object : View.OnClickListener { })
so method will be:
override fun onClick(v: View?) {
// Do something
}
instead yours:
fun onClick() {
}
Full code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myButton = findViewById<ImageButton>(R.id.live)
myButton.setOnClickListener(object : View.OnClickListener {
// When the button is pressed/clicked, it will run the code below
override fun onClick(v: View?) {
// Intent is what you use to start another activity
val intent = Intent(this, LiveActivity::class.java)
startActivity(intent)
}
})
}
}
I am creating an app which will load videos from youtube. The problem I am having is that i can not figure out how to instantiate a YoutubePlayerView inside an android fragment since my app only has one activity and all of the UI stuff is managed through fragments and navigation graph.
The only thing that i could figure out is instantiating YoutubePlayerView inside a YoutubeBaseActivity which will compromise the consistency of navigation since it will be a new activity.
I tried creating a YoutubePlayerFragment but it can't be set as a destination in a navigation graph.
So I will be thankful if anyone can help me figure out how to do this. :)
Thanks in advance.
For anyone having this issue today, this is what I did and it works perfectly for Activity and Fragment.
1. PLACE A FRAMELAYOUT
In your Activity or Fragment XML, place a FrameLayout. Simple enough.
<FrameLayout
android:id="#+id/main_player"
android:layout_width="match_parent"
android:layout_height="0dp"/>
2. INSTANTIATE YoutubePlayerSupportFragment AND ADD IT TO THE FRAME LAYOUT
Type in the next lines and if you get an error regarding the use of android.support.v4.Fragment you're going to have to create a new custom class of YoutubePlayerSupportFragment. I think this error happens because the YoutubePlayerSupportFragment class extends from android.support.v4.Fragment and I'm using androidx, I'm not sure. Still, I'm doing this inside a click listener, but you can do it anywhere else like in onViewCreated() or in onCreateView()
val youtubePlayerSupportFragment = YouTubePlayerSupportFragment.newInstance()
supportFragmentManager.beginTransaction()
.add(R.id.main_player, youtubePlayerSupportFragment).commit()
// these lines of code populates the FrameLayout with a brand new YoutubePlayerSupportFragment
// this code is actually for an Activity. If you want to do it for a fragment, replace the `supportFragmentManager` for `childFragmentManager` and it should work as well. It does for me.
So if these lines of code give you error, you have create a new class in this exact new path com.google.android.youtube.player and inside this directory create a new class. You can call it whatever you want. Just copy and paste this:
package com.google.android.youtube.player //<--- IMPORTANT!!!!
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.youtube.player.internal.ab
import java.util.*
class YouTubePlayerSupportFragmentX : Fragment(), YouTubePlayer.Provider {
private val a = ViewBundle()
private var b: Bundle? = null
private var c: YouTubePlayerView? = null
private var d: String? = null
private var e: YouTubePlayer.OnInitializedListener? = null
override fun initialize(var1: String, var2: YouTubePlayer.OnInitializedListener) {
d = ab.a(var1, "Developer key cannot be null or empty")
e = var2
a()
}
private fun a() {
if (c != null && e != null) {
c?.a(this.activity, this, d, e, b)
b = null
e = null
}
}
override fun onCreate(var1: Bundle?) {
super.onCreate(var1)
b = var1?.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE")
}
override fun onCreateView(var1: LayoutInflater, var2: ViewGroup?, var3: Bundle?): android.view.View? {
c = YouTubePlayerView(Objects.requireNonNull(this.activity), null, 0, a)
a()
return c
}
override fun onStart() {
super.onStart()
c?.a()
}
override fun onResume() {
super.onResume()
c?.b()
}
override fun onPause() {
c?.c()
super.onPause()
}
override fun onSaveInstanceState(var1: Bundle) {
super.onSaveInstanceState(var1)
(if (c != null) c?.e() else b)?.let { var2 ->
var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2)
}
}
override fun onStop() {
c?.d()
super.onStop()
}
override fun onDestroyView() {
this.activity?.let { c?.c(it.isFinishing) }
c = null
super.onDestroyView()
}
override fun onDestroy() {
if (c != null) {
val var1 = this.activity
c?.b(var1 == null || var1.isFinishing)
}
super.onDestroy()
}
private inner class ViewBundle : YouTubePlayerView.b {
override fun a(var1: YouTubePlayerView, var2: String, var3: YouTubePlayer.OnInitializedListener) {
e?.let { initialize(var2, it) }
}
override fun a(var1: YouTubePlayerView) {}
}
companion object {
fun newInstance(): YouTubePlayerSupportFragmentX {
return YouTubePlayerSupportFragmentX()
}
}
}
Line 36 is going to stay with a red underline but just dont pay atention to it. Couldn't find a way to solve it but it works fine.
In my case, I'm using my custom class so the lines of code I typed above turn out to be very similar:
val youtubePlayerSupportFragment = YouTubePlayerSupportFragmentX.newInstance() //notice this is my custom class, it ends with an X
supportFragmentManager.beginTransaction()
.add(R.id.main_player, youtubePlayerSupportFragment).commit()
3. INITIALIZE YOUTUBE PLAYER SUPPORT FRAGMENT
Now you should have your YoutubePlayerSupportFragment ready to go.
After being sure this works, you have to initialize the youtubePlayer. You have to do that just by calling youtubePlayerSupportFragment.initialize(apiKey, listener). Inside the parenthesis you have to set your apiKey and a listener. The api key can be generated easily, just google it if you don't have one. And for the listener you can do ctrl + shift + spacebar to open the smart suggestions, and it should pop up first. In my case it looks something like this:
val youtubePlayerSupportFragment = YouTubePlayerSupportFragmentX.newInstance()
supportFragmentManager.beginTransaction()
.add(R.id.main_player, youtubePlayerSupportFragment).commit()
youtubePlayerSupportFragment.initialize(
resources.getString(R.string.API_KEY), //IF YOU HAVE NO API KEY IT WONT WORK. But that's actually explained in the docs. So you can google it easily if you don't have one
object : YouTubePlayer.OnInitializedListener {
override fun onInitializationSuccess(
p0: YouTubePlayer.Provider?,
p1: YouTubePlayer?,
p2: Boolean
) {
p1?.loadVideo("9ET6R_MR1Ag") // string has to be https://www.youtube.com/watch?v=----------->9ET6R_MR1Ag<---------
}
override fun onInitializationFailure(
p0: YouTubePlayer.Provider?,
p1: YouTubeInitializationResult?
) {
layoutUtils.createToast(
applicationContext,
"ERROR INITIATING YOUTUBE"
)
}
})
And that's it! Lot of work, yeah, but it's worth it. It works perfectly even though the api documentation is poorly written and it has some bugs. But to the user looks exactly like youtube so it's going to give your app a very professional look. At least that's the feedback I got when I implemented it.
Important note:
If you want to play a video, and let the user be able to play other videos on cue, like on a click just like youtube, you have to store the youtubePlayer in a variable the moment it is initialized. For example:
var youtubePlayer: YoutubePlayer? = null // this is a global variable of your class
And when the player is successfully initiated, store it like this:
override fun onInitializationSuccess(
p0: YouTubePlayer.Provider?,
p1: YouTubePlayer?,
p2: Boolean
) {
p1?.loadVideo(video)
youtubePlayer = p1 // <------- this line here
}
Then in any part of your code you can just do youtubePlayer.loadVideo(anotherLink) and it will automatically stop the current video and start playing the new one.
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
I try to transfer device geolocation into fragment and using callback. But after set one time link is null
fun updateLocation(location:LatLng){
Log.d(TAG,"Update fragment "+ localUpdater+" "+location)
localUpdater?.tryOutLocation(location)
}
private inner class Receiver: BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val bundle = intent?.extras!![AppConstants.LOCATION_BUNDLE] as Bundle
val location = bundle.get(AppConstants.LOCATION_BUNDLE) as LatLng
updateLocation(location)
}
}
fun setUpdater(updater: IUpdateLocation){
this.localUpdater = updater
Log.d(TAG,"Update fragment1 "+localUpdater)
}
I declare callback:
private var localUpdater: IUpdateLocation? = null
Code interface:
interface IUpdateLocation {
fun tryOutLocation(location:LatLng)
}
Fragment code:
override fun onActivityCreated(savedInstanceState: Bundle?) {
act?.setUpdater(this)
super.onActivityCreated(savedInstanceState)
}
override fun tryOutLocation(location: LatLng) {
Log.d(TAG,"Update fragment map fragment "+location+" ")
if(gMap != null){
gMap?.uiSettings?.isZoomGesturesEnabled
gMap?.addMarker(MarkerOptions().position(location).title("Test"))
gMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(location, 20.0f))
}
First time callback is set and work (log)
but after first calling link is null. Why? How I can fix this problem?
I found where was mistake. I am using parent BaseActivity where is inner class Receiver. In SplashActivity that Inherited from BaseActivity I created object Receiver, and callback I created from fragment in MainActivity inherited from BaseActivity too. I had situation when first setting callback and after that was creating Reciever. I endured all processes in MainActivity and it work.