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.
Related
Let's take two generic methods:
fooA(), which onSuccess, it updates activity A
fooB(), which onSuccess invokes fooA()
that send an asynchronous Volley request each to my server.
I would like to avoid in Activity A this pattern:
class A : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_A)
fooB()
}
fun fooB(): {
volleyRequest() {
onSuccess(): fooA()
onFailure(): Log.e("ERROR")
}
}
fun fooA(): {
volleyRequest() {
onSuccess(): // do something on UI
onFailure(): Log.e("ERROR")
}
}
}
Instead, I would like to keep my code clean and define these requests in a separate RequestManager and have something like:
class A : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_A)
RequestManager.fooB().onSuccess().RequestManager.fooA().onSuccess() {
// do something on UI
}
}
}
Separating methods in this way would allow me to reuse fooA() and fooB() in other ways throughout my application.
Is it possible in Android (Kotlin) to deal with asynchronous tasks in this way?
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'm trying to use the new paging library for Android coding in Kotlin but am really stuck at the moment. My backend uses post method for connecting with the api calls and I'm trying to adapt the tutorials I've found using get but not being successful so far. Any help much appreciated indeed.
That's how my adapter is being called from my Fragment class but it's being always null.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setVerticalRecyclerView(rv_resources)
val itemViewModel = ViewModelProviders.of(this).get(ItemViewModel::class.java)
val adapter = ResourcesAdapter(activity as MainActivity)
itemViewModel.itemPagedList.observe(this, object : Observer<PagedList<Resource>> {
override fun onChanged(items: PagedList<Resource>?) {
adapter.submitList(items)
}
})
rv_resources.adapter = adapter
}
I feel the problem is probably coming from here:
class ItemDataSource : PageKeyedDataSource<Int, Resource>() {
override fun loadInitial(params: PageKeyedDataSource.LoadInitialParams<Int>, callback: PageKeyedDataSource.LoadInitialCallback<Int, Resource>) {
getResources()
}
override fun loadBefore(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, Resource>) {
getResources()
}
override fun loadAfter(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, Resource>) {
getResources()
}
private fun getResources() {
val jo = JsonObject()
jo.addProperty("page", 0)
jo.addProperty("page_size", 10)
GetAllResourceListAPI.postData(jo, object : GetAllResourceListAPI.ThisCallback {
override fun onSuccess(getResourceList: GetResourceList) {
Toast.makeText(App.getContext(), "onSuccess ${getResourceList.count}", Toast.LENGTH_SHORT).show()
}
override fun onFailure(failureMessage: String) {
Toast.makeText(App.getContext(), "onFailure", Toast.LENGTH_SHORT).show()
}
override fun onError(errorMessage: String) {
Toast.makeText(App.getContext(), "onError", Toast.LENGTH_SHORT).show()
}
})
}
Initially I'm trying to show any page within my adapter that's why pasted the same codes for loadInitial, loadBefore and loadAfter trying to tackle a problem at time if possible as currently my adapter shows empty even though I get a success from my api call. I may be missing something pretty obvious here but just can't see it as it's my first time using pagination and not very familiar with observers either.
I have a gist with a bit more of my code created here
Thanks very much for your help.
There is a specific implementation of interface when it comes to using Kotlin for Android: triggering an interface from a fragment. Consider the common scenario where a Fragment must communicate a UI action to the parent Activity. In Java, we would define the interface, create a "listener" instance of it, and implement/override actions in interface parents. It is the creating a listener instance that is not so straightforward to me. After some googling, I found an implementation example but I do not understand why it works. Is there a better way to do this? Why must it be implemented the way it is below?
class ImplementingFragment : Fragment(){
private lateinit var listener: FragmentEvent
private lateinit var vFab: FloatingActionButton
//Here is the key piece of code which triggers the interface which is befuddling to me
override fun onAttach(context: Context?) {
super.onAttach(context)
if(context is FragmentEvent) {
listener = context
}
}
//Notice the onClickListener using our interface listener member variable
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val v: View? = inflater.inflate(R.layout.fragment_layout, container, false)
vFab = v?.findViewById(R.id.fh_fab)
vFab.setOnClickListener{listener.SomethingHappened()}
return v
}
}
interface FragmentEvent{
fun SomethingHappened()
}
In this code lateinit var listener: FragmentEvent should be initialized and can't be null. So onAttach should looks like
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is FragmentEvent) {
listener = context
} else {
throw RuntimeException(context!!.toString() + " must implement FragmentEvent")
}
}
In this case if you forget to implement FragmentEvent in Activity you'll got an exception, otherwise callback is ready to use.
you could also use a try catch block and get the classic Java pattern
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as YourInterface
} catch (e: IllegalStateException) {
Log.d("TAG", "MainActivity must implement YourInterface")
}
}
the 'as' keyword can be used in to replicate java explicit typecasting in kotlin
val customItem = arguments?.getSerializable("KEY") as ArrayList<Custom>