Home > Blockchain >  How to update a TextView after an API response with Kotlin coroutines?
How to update a TextView after an API response with Kotlin coroutines?

Time:10-23

I am completely new to Kotlin, Coroutines and API calls, and I am trying to make an app based on this API.

My intention is to display the information of a game in my MainActivity, so I need ot fill some TextView for that purpose.

My API call and response system works perfectly well: the responses are OK and there are no errors, but the call is made using Kotlin's coroutines which won't let me update my UI after getting the response.

For the sake of simplicity, I am only attaching my MainActivity code, which is the source of the problem.

class MainActivity : AppCompatActivity() {
    private lateinit var b: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        b = ActivityMainBinding.inflate(layoutInflater)
        setContentView(R.layout.activity_main)
        listAllGames()
    }

    private fun listAllGames() {
        CoroutineScope(Dispatchers.IO).launch {
            val call = getRetrofit().create(APIService::class.java).listAllGames("")
            val games = call.body()
            runOnUiThread {
                if (call.isSuccessful) {
                    b.gameTitle.text = games?.get(0)?.title ?: "Dummy"
                    b.gameDesc.text = games?.get(0)?.short_description ?: "Dummy"
                    b.gameGenre.text = games?.get(0)?.genre ?: "Dummy"
                }
                else {
                    Toast.makeText(applicationContext, "ERROR", Toast.LENGTH_SHORT).show()
                    Log.d("mydebug", "call unsuccessful")
                }
            }
        }
    }

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://www.freetogame.com/api/games/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

Concretely, the listAllGames() method is the problem here: the app will build successfully, if a breakpoint is added inside the if (call.isSuccessful) block, it'll show the data correctly; but when running the app the display will be left blank forever.

Thanks to all in advance!

CodePudding user response:

In order to use view binding you need to pass the inflated view from the binding to the setContentView method. Otherwise you inflate the view with the binding but display the XML layout without the binding.

Check the documentation here:

private lateinit var binding: ResultProfileBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ResultProfileBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
}

(Source: Android Developer documentation - " View Binding Part of Android Jetpack.")

Change your onCreate method as followed:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    b = ActivityMainBinding.inflate(layoutInflater)
    setContentView(b.root)
    listAllGames()
}

CodePudding user response:

The other answer explains your problems with your view, but you also have some issues with the coroutine.

  • You should use lifecycleScope instead of creating a new scope. lifecycleScope will automatically cancel itself to avoid leaking the Activity if the activity is destroyed, such as during a screen rotation.
  • You should use Retrofit's call.await() suspend function instead of directly blocking a thread and having to specify a dispatcher. This also lets you leave things on the main dispatcher and directly update UI without having to use runOnUiThread.
    private fun listAllGames() {
        lifecycleScope.launch {
            val call = getRetrofit().create(APIService::class.java).listAllGames("")
            try { 
                val games = call.await()
                b.gameTitle.text = games?.get(0)?.title ?: "Dummy"
                b.gameDesc.text = games?.get(0)?.short_description ?: "Dummy"
                b.gameGenre.text = games?.get(0)?.genre ?: "Dummy"
            } catch (e: Exception) {
                Toast.makeText(applicationContext, "ERROR", Toast.LENGTH_SHORT).show()
                Log.d("mydebug", "call unsuccessful", e)
            }
        }
    }

And actually, you should move API calls like this into a ViewModel so they can keep running during a screen rotation. If you do that, the function in the ViewModel should update a LiveData or SharedFlow instead of UI elements. Then your Activity can observe for changes. If the call is started and the screen is rotated, the API call will keep running and still publish its changes to the new Activity's UI without having to restart.

  • Related