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.
Related
I am using Navigation component to navigate between two Fragments. The landing fragment has a recycler view and the detail fragment has a view pager. I am using a call back listener to trigger navigation action from the recycler view adapter.
The action to be trigger is a zoom event, with the library ZoomHelper ZoomHelper
When the event happens the app crashes with the error above.
However it works well with onclick event listener.
View Holder
class CampaignListViewHolder<T : ViewBinding>(private val binding: T) : RecyclerView.ViewHolder(binding.root) {
var campaignDetails: CampaignDetails? = null
#SuppressLint("ClickableViewAccessibility")
fun bindTo(campaignDetails: CampaignDetails?, position: Int, listener: ItemZoomListener) {
this.campaignDetails = campaignDetails
binding as CampaignItemLayoutBinding
binding.campaignNameTv.text = campaignDetails?.name
binding.campaignImageIv.load(campaignDetails?.image?.url) {
crossfade(true)
placeholder(R.drawable.westwing_placeholder)
}
ViewCompat.setTransitionName(binding.campaignImageIv, campaignDetails?.name)
binding.root.setOnClickListener {
if (campaignDetails != null) {
listener.navigate(position)
}
}
ZoomHelper.addZoomableView(binding.campaignImageIv)
ZoomHelper.getInstance().addOnZoomScaleChangedListener(object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
listener.navigate(position)
}
}
})
}
}
Landing Fragment
class LandingFragment : Fragment(R.layout.fragment_landing), ItemZoomListener, FragmentUiStateListener {
private val TAG by lazy { getName() }
private val binding by viewBinding(FragmentLandingBinding::bind)
private val campaignListViewModel: CampaignListViewModel by activityViewModels()
lateinit var campaignViewAdapter: CampaignListViewAdapter
lateinit var activityUiState: ActivityUiStateListener
lateinit var fragmentUiUpdate: FragmentUiStateListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
campaignViewAdapter = CampaignListViewAdapter(this)
fragmentUiUpdate = this
}
override fun onResume() {
super.onResume()
setupView()
setUpData()
}
private fun setUpData() {
setUpUiState(campaignListViewModel.campaignUiState, fragmentUiUpdate)
}
private fun setupView() {
val orientation = checkOrientation()
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.campaignLandscapeRcv?.apply {
layoutManager = GridLayoutManager(requireContext(), 2)
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 2))
}
} else {
binding.campaignListRcv?.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 1))
}
}
}
override fun navigate(position: Int) {
val direction = LandingFragmentDirections.actionLandingFragmentToCampaignDetailsFragment(position)
goto(direction)
}
I understand that one of the reason for the error is probably the zoom event calling the navigation controller multiple times but I can not figure how debug that and what could be a way around this.
As you guessed, the issue is most likely caused by the navController being fired multiple times. You can handle this by "navigating safely". Here's a sample implementation below:
fun NavController.safelyNavigate(#IdRes resId: Int, args: Bundle? = null) =
try { navigate(resId, args) }
catch (e: Exception) { Timber.e(e) }
}
You can then make your navigation call like this:
findNavController().safelyNavigate(your_id)
This way, any extra call to NavController.navigate() gets absorbed in the try and catch. Crash prevented :D
If someone had the same issues due to multiple clicks on the screen. It can be resolved by checking the current destination first before navigating
For example
Fragments A, B, and C
navigating from A to B while clicking on a button in fragment A that navigates to C might lead to crashes in some cases
for that you should check the current destination first as follows:
if(findNavController().currentDestination?.id==R.id.AFragment)
findNavController().navigate(
AFragmentDirections.actionAFragmentToCFragment()
)
If it is being called twice, then you can remove the 'ZoomHelper.OnZoomScaleChangedListener' before the navigation occurs.
I haven't tested the code below, but you can explore the library's code here https://github.com/Aghajari/ZoomHelper/blob/master/ZoomHelper/src/main/java/com/aghajari/zoomhelper/ZoomHelper.kt. You will find a method called 'removeOnZoomScaleChangedListener' which from what I understand, will remove a listener of type 'ZoomHelper.OnZoomScaleChangedListener'.
Ex.
val onZoomScaleChangedListener = object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
ZoomHelper.getInstance().removeOnZoomScaleChangedListener(onZoomScaleChangedListener) // Remove this event listener to avoid multiple triggers as you only need 1.
listener.navigate(position)
}
}
}
ZoomHelper.getInstance().addOnZoomScaleChangedListener(onZoomScaleChangedListener) // You can add the listener above like this.
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.
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
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 have 3 Parts to my Project: A Model that does calculations, some Fragments that display the UI and send Trigger to my third part, the main activity. I did all my Fragments with some interfaces like Communicating with Other Fragments.
However now I need one of the part of my Model to trigger some UI changes. And I don't know how to do that. Because my goal is to have one part of my Model send or trigger some functions so that the GUI gets updated but it doesn't know the GUI by itself. (it is totally independent from it)
In Main activity I override all the functions
class MainActivity : AppCompatActivity(), MimaFragment.elementSelectedListener, InstructionFragment.instructionSaveButtonClickedCallback , OptionFragment.optionSaveButtonClickedCallback, MimaFragment.UITrigger{
override fun abortOptions() {
extendNormal()
}
override fun updateMima() {
mimaFragment.updateView()
}
override fun normal() {
mimaFragment.drawArrows()
}}
Fragment exapmle:
class OptionFragment : Fragment() {
var optionCallback : optionSaveButtonClickedCallback? = null
interface optionSaveButtonClickedCallback{
fun updateMima()
fun abortOptions()
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
view?.findViewById(R.id.optionsAbort)?.setOnClickListener{
optionCallback?.abortOptions()
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
optionCallback = context as optionSaveButtonClickedCallback
} catch (e : ClassCastException){
throw ClassCastException(activity.toString() + " must implementoptionSaveButtonClickedCallback")
}
}
}
That is how you usually do it and it works fine. Now to my Question is there a way to do it just like that for a non Fragment class? I tryed it like this:
class MimaModul(name: String, description : String, context: Context) : Element(name, description) {
val uiTrigger : UITrigger? = null
init{
try {
uiTrigger = context as UITrigger
} catch (e : ClassCastException){
Log.d("ClassCastException","Didn't implement uiTrigger")
}
}
fun step(){
//it does some stuff here and then calls for example
uiTrigger?.normal()
}
interface UITrigger{
fun normal()
}
}
However as I expected the UITrigger cast does not work. (it always catches an exception) Do you have any ideas how to solve this. Or how else to do it?
ideally I want MimaFragment to implement the interface. But that didn't work either.
class MimaFragment : Fragment(), MimaModul.UITrigger {
//other stuff
override fun normal() {
drawArrows()
}
}
So when ever my Model is done with a step it should trigger some UI change. And I tryed to avoid just doing a loop and update all Elements based on their status because that would take forever. (Though I see this as my only options at the moment)
Let me know if I was unclear and i shall elaborate.