implementing an interface with callback on a fragment - android

I'm learning android programming and for practicing I'm trying to do a controller for some dc motors, then I did a customview for making a virtual joypad, which uses an interface and a callback for the ontouch listener.
The problem is, I'm working on my app using a single MainActivity as a navhost and then I'm navigating through different fragments, My customview just works when I override the interface method on my MainActivity but I can't make it works on my fragment, where I want to handle all the logic of the joypad.
I've a couple of days researching but most of the post that I've found are written on Java and I just can't make it work on Kotlin.
My custom view class
class KanJoypadView: SurfaceView, SurfaceHolder.Callback, View.OnTouchListener{
...kotlin
var joypadCallback: JoypadListener? = null
//the main constructor for the class
constructor(context: Context): super(context){
...
getHolder().addCallback(this)
setOnTouchListener(this)
...
}
//the interface for the main functionally of the view
interface JoypadListener {
fun onJoypadMove(x: Float, y: Float, src: Int){}
}
...
}
My MainActivity
class NavActivity : AppCompatActivity(), KanJoypadView.joypadListener {
...
//Overriding the Function from the interface,
//I just did this for debguging, but I dont want this override here
override fun onJoypadMove(x: Float, y: Float, src: Int) {
Log.d(src.toString(), y.toString()) //** I wanna do this in my Fragment, not in my activity **
}
}
My Fragment
class JoystickFragment : Fragment(), KanJoypadView.joypadListener {
...
var enginesArray = arrayOf(0.toFloat(), 0.toFloat(), 0.toFloat(), 0.toFloat())
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
val binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_joystick, container, false
)
binding.leftJoypad.joypadCallback = (container?.context as KanJoypadView.JoypadListener?)
lJoypad = binding.leftJoypad.id
}
/*what I really want to do, but it is not happening as it is just happenning the
override from the NavActivity, which I dont need, and not from here which I need*/
override fun onJoypadMove(x: Float, y: Float, src: Int) {
if (src == lJoypad) {
if (y >= 0) {
enginesArray[0] = 1.toFloat()
enginesArray[1] = y
} else if (y < 0) {
enginesArray[0] = 0.toFloat()
enginesArray[1] = y
}
if (src == rJoypad) {
if (y >= 0) {
enginesArray[0] = 1.toFloat()
enginesArray[1] = y
} else if (yAxis < 0) {
enginesArray[0] = 0.toFloat()
enginesArray[1] = y
}
Log.d("Engines array", enginesArray.toString())
}
}
}
}
Also I've tried to make a function in the fragment, and then call that function from the onMoveJoypad method from the Activity, but also I couldn't made it work. I'll appreciate any help or advice on how to implement this, thanks in advance !

This:
if (context is joypadListener){
is a very hacky and error prone way to get a listener reference. It’s also very limiting because it makes it impossible for the listener to be anything but the Activity that created the view. Don’t do this!
You already have a joypad listener property. Just remove the private keyword so any class can set any listener it wants from the outside. Remove that whole try/catch block. When it’s time to call the listener’s function, use a null-safe ?. call to do it so it gracefully does nothing if a callback hasn’t been set yet.
Side note: all class and interface names should start with a capital letter by convention. Your code becomes very hard to read and interpret if you fail to follow this convention.
Also, I advise you to avoid using the pattern of making a class implement an interface to serve as a callback for one of the objects it is manipulating or for its own internal functions. You’re doing this twice in your code above. Your custom view does it for its own touch listener, and your Activity does it to serve as the joypad listener.
The reason is that it publicly exposes the class’s inner functionality and reduces modularity. It can make unit testing more difficult. It needlessly exposes ways to misuse the class you’ve designed. It’s not as big deal for Activities to do this because you rarely if ever work with Activity instances from the outside anyway. But it’s ugly for a view class to do it.
The alternative is to implement the interface as an anonymous object or lambda so the functionality of the callback is hidden from outside classes.
Edit: How to do this in your fragment
If you want to follow my advice above, don't implement callback interfaces in your classes. Use lambdas or anonymous classes instead.
class JoystickFragment : Fragment() {
//...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//...
adapter.joypadCallback = joypadListener
//...
}
private val joypadListener = KanJoypadView.JoypadListener { x, y, src ->
if (src == lJoypad) {
if (y >= 0) {
enginesArray[0] = 1.toFloat()
enginesArray[1] = y
} else if (y < 0) {
enginesArray[0] = 0.toFloat()
enginesArray[1] = y
}
if (src == rJoypad) {
if (y >= 0) {
enginesArray[0] = 1.toFloat()
enginesArray[1] = y
} else if (yAxis < 0) {
enginesArray[0] = 0.toFloat()
enginesArray[1] = y
}
Log.d("Engines array", enginesArray.toString())
}
}
}
}

Related

Can't change the value of MutableList in a Kotlin class

I want to add the random generated integer into my MutableList in Player class when I use the random integer generator method located in Player class in my fragment then I want to pass this MutableList to Fragment with using Livedata(I'm not sure if i'm doing right with using livedata).Then show the MutableList in TextView.But MutableList returns the default value not after adding.
So what am i doing wrong ? What should i do ?
Thank you
MY CLASS
open class Player {
//property
private var playerName : String
private var playerHealth : Int
var playerIsDead : Boolean = false
//constructor
constructor(playerName:String,playerHealth:Int){
this.playerName = playerName
this.playerHealth = playerHealth
}
var numberss: MutableList<Int> = mutableListOf()
fun attack(){
//Create a random number between 1 and 10
var damage = (1..10).random()
//Subtract health points from the opponent
Log.d("TAG-DAMAGE-WRITE","$damage")
numberss.add(damage)
Log.d("TAG-DAMAGE-ADD-TO-LIST","$numberss")
Log.d("TAG-NUMBER-LIST-LATEST-VERSION","$numberss")
}
}
MY FRAGMENT
class ScreenFragment : Fragment() {
var nickname : String? = null
private lateinit var viewModel : DenemeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_screen, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(DenemeViewModel::class.java)
arguments?.let {
nickname = ScreenFragmentArgs.fromBundle(it).nickName
}
sFtextView.text = "Player : $nickname"
action()
}
private fun action(){
val health1 = (1..50).random()
val health2 = (1..50).random()
val superMan = Superman("Dusman",health1,)
while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
viewModel.setData()
observeLiveData()
superMan.playerIsDead = true
}
}
fun observeLiveData(){
viewModel.damageList.observe(viewLifecycleOwner, Observer { dmgList ->
dmgList?.let {
sFtextsonuc.text = it.toString()
Log.d("TAG-THE-LIST-WE'VE-SENT-TO-FRAGMENT","$it")
}
})
}
}
MY VIEWMODEL
class DenemeViewModel : ViewModel() {
val damageList:MutableLiveData<MutableList<Int>> = MutableLiveData()
fun setData(){
damageList.value = Superman("",2).numberss
Log.d("TAG-VIEWMODEL","${damageList.value}")
}
}
MY LOG
PHOTO OF THE LOGS
Your Superman is evidently part of some ongoing game, not something that should be created and then destroyed inside the action function. So you need to store it in a property. This kind of state is usually stored in a ViewModel on Android so it can outlive the Fragment.
Currently, you are creating a Superman in your action function, but anything created in a function is automatically sent do the garbage collector (destroyed) if you don't store the reference in a property outside the function.
Every time you call the Superman constructor, such as in your line damageList.value = Superman("",2).numberss, you are creating a new instance of a Superman that has no relation to the one you were working with in the action() function.
Also, I recommend that you do not use LiveData until you fully grasp the basics of OOP: what object references are, how to pass them around and store them, and when they get sent to the garbage collector.
So, I would change your ViewModel to this. Notice we create a property and initialize it with a new Superman. Now this Superman instance will exist for as long as the ViewModel does, instead of only inside some function.
class DenemeViewModel : ViewModel() {
val superman = Superman("Dusman", (1..50).random())
}
Then in your frgament, you can get this same Superman instance anywhere you need to use it, whether it be to deal some damage to it or get the current value of its numberss to put in an EditText.
Also, I noticed in your action function that you have a while loop that repeatedly deals damage until Superman is dead. (It also incorrectly observes live data over and over in the loop, but we'll ignore that.) The problem with this is that the while loop is processed to completion immediately, so you won't ever see any of the intermediate text. You will only immediately see the final text. You probably want to put some delays inside the loop to sort of animate the series of events that are happening. You can only delay easily inside a coroutine, so you'll need to wrap the while loop in a coroutine launch block. In a fragment when working with views, you should do this with viewLifecycleOwner.lifecycleScope.launch.
Finally, if you set playerIsDead to true on the first iteration of the loop, you might as well remove the whole loop. I'm guessing you want to wrap an if-condition around that line of code. But since your code above doesn't modify player health yet, there's no sensible way to determine when a player should be dead, so I've commented out the while loop
private fun action() = viewLifecycleOwner.lifecycleScope.launch {
val superMan = viewModel.superman
//while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
delay(300) // give user a chance to read the text before changing it
sFtextsonuc.text = superMan.numberss.joinToString()
delay(300) // give user a chance to read the text before changing it
// TODO superMan.playerIsDead = true
//}
}

How to use customized onClick in data binding?

I am using data binding and having trouble solving multiple quick click. I do not want to put a logic in every click instead, I want to create a solution once and expect it to work throughout my project.
I found one solution here. Code snippet from that page is as follows:
class SafeClickListener(
private var defaultInterval: Int = 1000,
private val onSafeCLick: (View) -> Unit
) : View.OnClickListener {
private var lastTimeClicked: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return
}
lastTimeClicked = SystemClock.elapsedRealtime()
onSafeCLick(v)
}
}
And using extension funciton:
fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) {
val safeClickListener = SafeClickListener {
onSafeClick(it)
}
setOnClickListener(safeClickListener)
}
Now, for any view we can simply call:
anyView.setSafeOnClickListener {
doYourStuff()
}
Which is awesome. But it only applies if I am calling setOnClickListener to a view but I am using data binding. Where I am using something like this:
android: onClick="#{(view) -> myViewModel.UIEvents(SomeUIEvent.showDialog)}"
I am aware that if I can create a binding adapter, I would be able to solve the problem. But, I couldn't make one that works.
How can I achieve something that I can use with data binding and that works globally like the above example?
Thanks
maybe you can use #BindingAdapter.

How can I get the value of a LivaData<String> at once in Android Studio?

savedRecordFileName is a variable of LivaData<String>, I hope to get the value of savedRecordFileName at once in Code A.
You know that LiveData variable is lazy, maybe the value of savedRecordFileName is null in binding.btnStop.setOnClickListener { }, so the code in binding.btnStop.setOnClickListener { } will not be fired when the value of savedRecordFileName is null.
I hope that the code in binding.btnStop.setOnClickListener { } can always be fired, how can I do?
BTW, I think the Code B is not suitable because the value of savedRecordFileName maybe changed by other function.
Code B
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}
Code A
class FragmentHome : Fragment() {
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.value?.let{
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}
...
return binding.root
}
}
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
val savedRecordFileName: LiveData<String> = mDBVoiceRepository.getTotalOfVoice().map {
mApplication.getString(R.string.defaultName, (it+1).toString())
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
}
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoice(): LiveData<Long>
}
Add Content
To Ridcully: Thanks!
I think your way "move all of that into the viewmodel class" is good !
I think it will be OK even if the filename is livedata in your Code C, right?
Code C
viewModelScope.launch(Dispatchers.IO) {
filename = dao.getFilename() // without livedata. I think it will be OK even if the filename is livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
result.postValue(true)
}
As you said, you should do Room queries in background to avoid blocking ui thread. But you shouldn't stop there. Do as much work as possible in a background thread. In your case, on button click, start running a background thread that does all the work: getting name from database (with direct query, which is possible now as you are running in background), getting the default voice, and adding it, to whatever it is you are adding it to.
You should also move all of that into the viewmodel class.
btnStop.setOnClickListener {
viewmodel.stop()
}
In viewmodel something like this (java / pseudo code like):
void stop() {
runinbackgroundthread {
filename = dao.getFilename() // without livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
// If you want the action to have a reault, observable by ui
// use a MutableLiveData and set it here, via postValue()
// as we are still in background thread.
result.postValue(true)
}
}
On how to actually implement my pseudo code runinbackgroundthread see some official guide here: https://developer.android.com/guide/background/threading
In short, with Kotlin coroutines it should go like this:
viewModelScope.launch(Dispatchers.IO) {
filename = dao.getFilename() // without livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
result.postValue(true)
}
Solution 1
A simple/tricky/dirty solution would be an empty observe in onViewCreated like:
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){ }
which causes the LiveData to be observed, so it will be queried by Room because it is active and hopefully when you click on the button, the LiveData is filled.
Solution 2
But my favorite approach would be a reactive one. I don't know if you want to take a reactive look toward your code or not, but you can use MediatorLiveData (or take advantage of a library like this) to solve this situation reactively.
Let there be another LiveData in your VM named btnStopClicked and each click changes the value of that LiveData. So, your code may look like this:
class FragmentHome : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.btnStop.setOnClickListener {
mHomeViewModel.btnStopClicked.value = null //we don't care about the value
}
mHomeViewModel.addVoiceEvent.observe(viewLifecycleOwner){
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
...
return binding.root
}
}
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
val savedRecordFileName: ... //whatever it was
val btnStopClicked = MutableLiveData<Any>()
val addVoiceEvent = savedRecordFileName
.combineLatestWith(btnStopClicked){srfn, _ -> srfn}
.toSingleEvent()
}
Also do not forget toSingleEvent because if you don't turn it into an event, it would get called unintentionally in some situations (look at this).
TL;DR - Code A is better, with extra additions.
As you already know, LiveData is not guaranteed to have a non-null value right after instantiation. Ideally, as for best practice, it is good to always treat LiveData value as ever changing (i.e - assume direct value access twice will produce different values). This is intended behavior as LiveData is designed to be observable instead of you dealing with immediate value.
Hence, listen to the LiveData and update your UI accordingly. For instance, disable or hide the stop button when there's no data inside it.
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner) {
binding.btnStop.isEnabled = it != null
}
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.value?.also {
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}

Kotlin Errors After Converting from Java

I do not have very much experience with Kotlin, been doing most of my development in Java. I have converted my ArtistFragment to Kotlin in order to solve another issue. But after converting it I am getting to errors that I do not know how to resolve.
The first one is Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time and the second one is Cannot create an instance of an abstract class
I searched through Stackoverflow, but since I don't really understand the what the problem is I am not sure if they are relevant to my problems.
Here is my converted ArtistFragment:
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
private var recyclerView: RecyclerView? = null
private var adapter: ArtistAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
LoadData().execute("")
return view
}
abstract inner class LoadData : AsyncTask<String?, Void?, String>() {
protected fun doInBackground(vararg strings: String): String {
if (activity != null) {
adapter = ArtistAdapter(activity, ArtistLoader().artistList(activity))
}
return "Executed"
}
override fun onPostExecute(s: String) {
recyclerView!!.adapter = adapter
if (activity != null) {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
}
}
override fun onPreExecute() {
super.onPreExecute()
}
}
}
I am seeing Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time here in this section: recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
I am seeing Cannot create an instance of an abstract class here in this section LoadData().execute("")
I hoping someone can explain these error and how to fix them.
Thanks
As your recyclerview is a nullable property, You need to modify your code as below -
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
Remove abstract from class
inner class LoadData : AsyncTask<String?, Void?, String>()
Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time is because recyclerView is a var. Kotlin has the concept of mutable (var) and immutable (val) variables, where a val is a fixed value, but a var can be reassigned.
So the issue is that recyclerView is a nullable type (RecyclerView?) and it could be null - so you need to make sure it's not null before you access it, right? findViewById returns a non-null RecyclerView, which gets assigned to recyclerView, so the compiler knows it can't be null.
But then it gets to the next line - what if the value of recyclerView has changed since the previous line? What if something has modified it, like on another thread? It's a var so that's possible, and the compiler can't make any guarantees about whether it's null or not anymore. So it can't "smart cast to RecyclerView!" (! denotes non-null) and let you just treat it as a non-null type in your code. You have to null check it again.
What Priyanka's code (recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))) does is null-checks recyclerView, and only makes the call if it's non-null. Under the hood, it's copying recyclerView to a temporary val (which isn't going to change) and checks that, and then executes the call on that.
This is a pretty standard thing with Kotlin, and not just for nullable types - any var could possibly change at any moment, so it's typical to assign it to a temporary variable so you can do the stuff on that stable instance. Kotlin has a bunch of scope functions which are convenient for doing things on an object, and one of the benefits is that you know the reference you're acting on isn't going to change halfway through

Android - Trigger MainActivity to do something from custom View

I would like to let my MainActivity know that there is something to do (e.g. calculate stuff) from a custom View. The View detects user inputs through touch and the MainActivity has to update certain user controls with a calculation from those View-Values. Basically I did an override on onTouchEvent:
override fun onTouchEvent(event: MotionEvent?): Boolean {
val x = event?.x
val y = event?.y
val dX = x?.minus(prevX)
val dY = y?.minus(prevY)
if(dX!! > 0){
lowerBound = x.toInt()
} else{
upperBound = x.toInt()
}
prevX = x!!.toInt()
prevY = y!!.toInt()
this.invalidate() //tell view to redraw
return true
}
How can I let MainActivityknow that lowerBound and upperBound updated?
As already recommended in the comments I'll go a little bit deeper in "hot to use a listener".
First of all, a "Listener" is nothing more than a interface which get called if something happen elsewhere. The best example is the View.setOnClickListener(View).
How to use that in your case?
Simply define a interface (best location is here inside your custom View):
interface OnBoundsUpdatedListener {
fun onBoundsUpdated(upperBound: Int, lowerBound: Int)
}
Then you have to create a property in your CustomView and call the Listener if the bounds have changed :
// Constructor and stuff
val onBoundsUpdateListener: OnBoundsUpdatedListener? = null
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
onBoundsUpdateListener?.onBoundsUpdated(upperBound, lowerBound)
...
}
In your Activtiy you can find that View and set the listener:
val myCustomView = findViewById<MyCustomView>(R.id.id_of_your_customview)
myCustomView.onBoundsUpdateListener = object : OnBoundsUpdatedListener {
override fun onBoundsUpdated(upperBound, lowerBound) {
// Get called if its called in your CustomView
}
}
Note: The code can be simplified. But that is the basic stuff 😉

Categories

Resources