Search code examples
androidclasskotlinandroid-activityandroid-view

Android (Kotlin) - Cannot access view from another class


I have an Activity Class called MusicPlayer and an external class called MpPlayer. In this latter, my goal is to change the layout from MusicPlayer. However, even though I pass the context from MusicPlayer to MpPlayer, the app continues to return a java.lang.NullPointerException whenever the MpPlayer class tries to access the layout.

I've cleansed quite a bit the code so it's easy to read the problem;

My main class with the view:

 open class MusicPlayer : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_musicplayer)

            mp = MpPlayer(this@MusicPlayer)
            mp.loadMusic //error here
    }
}

And my second class:

class MpPlayer(private var playerReference: MusicPlayer){

    init{
        playerReference = MusicPlayer()
    }

    fun loadMusic(url: String){
            musicPlayer.apply {
                reset()
                setDataSource(url)
                prepareAsync()
                setOnPreparedListener { mp ->
                    totaltime = mp.duration
                    playerReference.seekBar.max = totaltime         //error here
                    playerReference.txt_musicName.text = playerReference.musicNameList[musicCounter]
                    playerReference.txt_musicArtist.text = playerReference.musicArtistList[musicCounter]
                    playerReference.btn_musicplayPause.setImageResource(R.drawable.ic_play)

                    if (wanting2Play) {
                        mp.start()
                        playerReference.btn_musicplayPause.setImageResource(R.drawable.ic_pause)
                    }

                }
                setOnCompletionListener { skipNext() }
            }
        }
}

The Logcat:

2020-07-13 17:27:11.278 21742-21742/com.android.slowfy E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.android.slowfy, PID: 21742
    java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.ApplicationInfo android.content.Context.getApplicationInfo()' on a null object reference
        at android.content.ContextWrapper.getApplicationInfo(ContextWrapper.java:159)
        at android.view.ContextThemeWrapper.getTheme(ContextThemeWrapper.java:157)
        at android.content.Context.obtainStyledAttributes(Context.java:675)
        at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:692)
        at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:659)
        at androidx.appcompat.app.AppCompatDelegateImpl.findViewById(AppCompatDelegateImpl.java:479)
        at androidx.appcompat.app.AppCompatActivity.findViewById(AppCompatActivity.java:214)
        at com.android.slowfy.MpPlayer$loadMusic$$inlined$apply$lambda$1.onPrepared(MpPlayer.kt:41)
        at android.media.MediaPlayer$EventHandler.handleMessage(MediaPlayer.java:3367)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2020-07-13 17:32:11.329 21742-21775/com.android.slowfy E/Surface: queueBuffer: error queuing buffer to SurfaceTexture, -19
2020-07-13 17:32:11.330 21742-21775/com.android.slowfy E/EGL_emulation: tid 21775: swapBuffers(570): error 0x300d (EGL_BAD_SURFACE)

Any thoughts on this would be greatly appreciated :)


Solution

  • Here in your init block, you have overridden your passed in Activity with a newly instantiated one:

    class MpPlayer(private var playerReference: MusicPlayer){
    
        init{
            playerReference = MusicPlayer()
        }
    

    First, remove this init block. Note that Activities should never be instantiated directly, because then they are not set up properly to be able to use their context. Also, you need to allow the Activity to remove itself as a listener when it is closed, so make the property nullable and set it to null when the Activity is destroyed. Otherwise, your MPPlayer class may try to update views when the Activity is already gone, and it will also leak the Activity.

    class MpPlayer(var playerReference: MusicPlayer?){
    
    open class MusicPlayer : AppCompatActivity() {
        //...
        override fun onDestroy() {
            super.onDestroy()
            mp.playerReference = null
        }
    }
    

    I'll also note that passing an Activity to a music player is poor use of encapsulation. You're tightly coupling these two classes like spaghetti, when the music player should be able to operate independently without any knowledge of View classes. I recommend creating a listener class for various MusicPlayer events and have the Activity implement them, like this:

    class MpPlayer(var eventListener: EventListener?){
    
        interface EventListener {
            fun onPlayerPrepared(mediaPlayer: MediaPlayer)
            fun onPlaybackCompleted(mediaPlayer: MediaPlayer)
            //etc. anything views in Activity might want to respond to
        }
    
        fun loadMusic(url: String){
                musicPlayer.apply {
                    reset()
                    setDataSource(url)
                    prepareAsync()
                    setOnPreparedListener { mp ->
                        eventListener?.onPlayerPrepared(mp)
                    }
                    setOnCompletionListener { 
                        eventListener?.onPlaybackCompleted(mp)
                        skipNext() 
                    }
                }
            }
    }
    

    I also recommend putting the word "Activity" at the name of any Activity class to avoid confusion.

    class MusicPlayerActivity : AppCompatActivity(), MpPlayer.EventListener {
    
        override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                setContentView(R.layout.activity_musicplayer)
    
                mp = MpPlayer(this)
                mp.loadMusic()
        }
    
        override fun onPlayerPrepared(mediaPlayer: MediaPlayer){
            totaltime = mp.duration
            seekBar.max = totaltime
            txt_musicName.text = playerReference.musicNameList[musicCounter]
            txt_musicArtist.text = playerReference.musicArtistList[musicCounter]
            btn_musicplayPause.setImageResource(R.drawable.ic_play)
            //...
        }
    
        override fun onPlaybackCompleted(mediaPlayer: MediaPlayer) {
            //...
        }
    
        override fun onDestroy() {
            super.onDestroy()
            mp.eventListener = null
        }
    }