I just updated my Android Studio to version 3.2 and followed instructions to use androidx.
I've been using a Youtube fragment inside a Fragment activity and everything worked perfectly but, after the update, these 3 simple lines now give me the error "Cannot resolve method 'add(...)'":
YouTubePlayerSupportFragment youTubePlayerFragment = YouTubePlayerSupportFragment.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
...and when i try to use "replace" instead of "add" it says: "Wrong 2nd argument type. Found: 'com.google.android.youtube.player.YouTubePlayerSupportFragment', required: 'androidx.fragment.app.Fragment'"
...which makes me think that the problem has to do with the new AndroidX feature.
The problem is that the add method wants the second parameter of type:
androidx.fragment.app.Fragment
...but the YouTubePlayerSupportFragment returns a:
android.support.v4.app.Fragment
Does anyone know how to solve this problem?
Is there a way to cast the "android.support.v4.app.Fragment" into the "androidx.fragment.app.Fragment"?
Just use transaction.replace. Ignore the error, it'll work. Google hasn't refactored youtube api library to androidx yet.
Just copy the original java file (com.google.android.youtube.player.YouTubePlayerFragment) to your project to the same package but different class name etc. com.google.android.youtube.player.YouTubePlayerFragmentX, and update the extends class from android.app.Fragment to androidx.fragment.app.Fragment.
The implementation is the same:
YouTubePlayerFragmentX youTubePlayerFragment = YouTubePlayerFragmentX.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
Tested... it's working.
I've fixed it by following the #Hosszful answer,
I made it easy by just using this file, https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492
Replace .add
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
with this .replace
transaction.replace(R.id.youtube_fragment, youTubePlayerFragment).commit();
and copy this class to your project folder (it may need to create the following folders)
java -> com -> google -> android -> youtube -> player -> (here name of)
YouTubePlayerSupportFragmentX.java
then in code replace
YouTubePlayerSupportFragment
to
YouTubePlayerSupportFragmentX.
Many thanks to both #Hosszuful and #Mehdi. I have followed your advice and it worked very nicely.
A few weeks after I asked this question I "translated" my app to Kotlin and, therefore, I tried to translate your answer as well.
This is what I ended up with and it's working for me.
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()
}
}
}
There may be better ways to write it down and any help on that regard would be mostly appreciated but, if anyone else was looking for the Kotlin version of this problem's solution, this code should do.
PS: I'm gonna leave #Mehdi's answer as the accepted one because he's also sharing credits with #Hosszuful and because my answer is just the Kotlin version of what they suggest.
I got it working by following code chunk.
Object obj =
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
if (obj instanceof YouTubePlayerSupportFragment)
youTubePlayerFragment = (YouTubePlayerSupportFragment) obj;
During debugging I found that the fragmentmanager was coming to be instance of YouTubePlayerSupportFragment only. But compiler was not able to cast it when I would write
(YouTubePlayerSupportFragment)
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
The above code chunk (instanceof ) worked fine.
Suggested solutions did not work, till I tried the comment from Bek: Pierfrancesco Soffritti's android-youtube-player that is maintained and works without a hitch.
Related
Currently I'm facing a problem getting data from a child fragment to it's parent fragment, I saw that the best option is using setFragmentResult but because setFragmentResultListener needs to be in STARTED state at parentFragment(doen't happen because it is stoped when replaced by another fragment) I see that the only option is to use popBackStack() and then the listener gets triggered. The thing is that I don't wanna use popBackStack()
Can anyone help me?
PS: No, I don't want to use viewModel in this case to keep data.
Listener:
class ResultListenerFragment : Fragment() {
val viewModel : SomeViewModel by viewModels()
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
//verify values
viewModel.repeat()
}
//in case of an error
viewModel.getError().observe(viewLifecycleOwner,{
requireActivity()
.supportFragmentManager
.beginTransaction()
.replace(R.id.framelayout, ErrorFragment.newInstance(it).commit()
}
}
}
Triggerer:
class ErrorFragment: Fragment(R.layout.fragment_error) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.some_tv).text = arguments.getString(keyError)
view.findViewById(R.id.result_button).setOnClickListener {
val result = "retry"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
companion object {
private const val keyError = "errorKey"
fun newInstance(error:String): ErrorFragment{
val args = Bundle().apply {
putString(keyError, error)
}
}
return ErrorFragment().apply{arguments = args}
}
}
I have created this extension function for navigating between fragments which is pretty straight forward but somehow it's not working. it's not doing anything and nothing changes when I click the button. I think the problem is with this#navigate argument but I don't see why that should be a problem.
fun Fragment.navigate(): Int? {
return fragmentManager?.run {
beginTransaction()
.replace(
R.id.my_container,
this#navigate,
this#navigate::class.simpleName
)
.commit()
}
}
and the usage is like this
class TestTwoFragment : Fragment(R.layout.fragment_test_two) {
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
goto_three.setOnClickListener {
TestThreeFragment
.newInstance()
.navigate()
}
}
You're using the fragmentManager of the new fragment which will be null if the fragment hasn't been added yet, like in your case. Since you're using ?.run, nothing happens and the method returns null.
Consider adding a fragment manager parameter to your method:
fun Fragment.navigate(fm: FragmentManager): Int? {
return fm.run {
beginTransaction()
.replace(
R.id.my_container,
this#navigate,
this#navigate::class.simpleName
)
.commit()
}
}
And then:
TestThreeFragment
.newInstance()
.navigate(getParentFragmentManager())
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 am working with the MVVM architecture.
The code
When I click a button, the method orderAction is triggered. It just posts an enum (further logic will be added).
ViewModel
class DashboardUserViewModel(application: Application) : SessionViewModel(application) {
enum class Action {
QRCODE,
ORDER,
TOILETTE
}
val action: LiveData<Action>
get() = mutableAction
private val mutableAction = MutableLiveData<Action>()
init {
}
fun orderAction() {
viewModelScope.launch(Dispatchers.IO) {
// Some queries before the postValue
mutableAction.postValue(Action.QRCODE)
}
}
}
The fragment observes the LiveData obj and calls a method that opens a new fragment. I'm using the navigator here, but I don't think that the details about it are useful in this context. Notice that I'm using viewLifecycleOwner
Fragment
class DashboardFragment : Fragment() {
lateinit var binding: FragmentDashboardBinding
private val viewModel: DashboardUserViewModel by lazy {
ViewModelProvider(this).get(DashboardUserViewModel::class.java)
}
private val observer = Observer<DashboardUserViewModel.Action> {
// Tried but I would like to have a more elegant solution
//if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED)
it?.let {
when (it) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.action.observe(viewLifecycleOwner, observer)
// Tried but still having the issue
//viewModel.action.reObserve(viewLifecycleOwner, observer)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
// Tried but still having the issue
//viewModel.action.removeObserver(observer)
}
private fun navigateToQRScanner() {
log("START QR SCANNER")
findNavController().navigate(LoginFragmentDirections.actionLoginToPrivacy())
}
}
The problem
When I close the opened fragment (using findNavController().navigateUp()), the Observe.onChanged of DashboardFragment is immediately called and the fragment is opened again.
I have already checked this question and tried all the proposed solutions in the mentioned link (as you can see in the commented code). Only this solution worked, but it's not very elegant and forces me to do that check every time.
I would like to try a more solid and optimal solution.
Keep in mind that in that thread there was no Lifecycle implementation.
The issue happens because LiveData always post the available data to the observer if any data is readily available. Afterwords it will post the updates. I think it is the expected working since this behaviour has not been fixed even-though bug raised in issue tracker.
However there are many solutions suggested by developers in SO, i found this one easy to adapt and actually working just fine.
Solution
viewModel.messagesLiveData.observe(viewLifecycleOwner, {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
//Do your stuff
}
})
That's how LiveData works, it's a value holder, it holds the last value.
If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this
class ConsumableValue<T>(private val data: T) {
private val consumed = AtomicBoolean(false)
fun consume(block: ConsumableValue<T>.(T) -> Unit) {
if (!consumed.getAndSet(true)) {
block(data)
}
}
}
then you define you LiveData as
val action: LiveData<ConsumableValue<Action>>
get() = mutableAction
private val mutableAction = MutableLiveData<ConsumableValue<Action>>()
then in your observer, you'd do
private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
it?.consume { action ->
when (action) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
UPDATE
Found a different and still useful implementation of what Frances answered here. Take a look
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