I was trying to build a RecyclerView using a response from Retrofit. But, I ran into an issue that my Recycler turns up empty white while my log shows that I have data in my ArrayList from the network response. (I do not want to set up an MVVM yet until I get comfortable with Kotlin.)
PlaylistRecyclerAdapter
class PlaylistRecyclerAdapter (private val playListNames: Array<String>) :
RecyclerView.Adapter<PlaylistRecyclerAdapter.PlayListViewHolder>() {
// Describes an item view and its place within the RecyclerView
class PlayListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val playlistTextView: TextView = itemView.findViewById(R.id.playlist_name_text)
fun bind(word: String) {
playlistTextView.text = word
}
}
// Returns a new ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlayListViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.playlist_name_item, parent, false)
return PlayListViewHolder(view)
}
// Returns size of data list
override fun getItemCount(): Int {
return playListNames.size
}
// Displays data at a certain position
override fun onBindViewHolder(holder: PlayListViewHolder, position: Int) {
holder.bind(playListNames[position])
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
val templist = getPlaylistItems()
//Log.d("RESPONSE", "onCreate: "+templist.get(0).toString())
recyclerView.adapter = PlaylistRecyclerAdapter(templist.toTypedArray())
recyclerView.adapter?.notifyDataSetChanged()
}
private fun getPlaylistItems(): ArrayList<String> {
var playlisttitles = ArrayList<String>()
var BASE_URL = "https://flicastdemo.s3.amazonaws.com/jwplayer/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(HomeWebService::class.java)
val call = service.getHomeContent()
var home = HomeRoot()
call.enqueue(object : Callback<HomeRoot> {
override fun onResponse(call: Call<HomeRoot>, response: Response<HomeRoot>) {
if (response.code() == 200) {
home = response.body()
if(!home.equals(null))
{
//Log.e("HOME", "val: " + home.toString())
for (i in 0 until home.content.size){
val BASE_URL = "https://cdn.jwplayer.com/v2/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(PlaylistWebService::class.java)
val call = service.getPlayListItem(home.content.get(i).playlistId) //"1QhdrFVq"
call.enqueue(object : Callback<PlaylistRoot> {
override fun onResponse(call: Call<PlaylistRoot>, response: Response<PlaylistRoot>) {
if (response.code() == 200) {
var playlistinfo : PlaylistRoot = response.body();
playlisttitles.add(playlistinfo.title)
Log.e("PlaylistTitle!", "onResponseTitle: "+playlistinfo.title)
}
}
override fun onFailure(call: Call<PlaylistRoot>, t: Throwable) {
Log.d("NO!NO!NO!", "onResponse: "+"NO!")
playlisttitles.add("No Playlist")
}
})
}
}
}
}
override fun onFailure(call: Call<HomeRoot>, t: Throwable) {
Log.d("NO!NO!NO!", "onResponse: "+"NO!")
}
})
return playlisttitles
}
}
Retrofit returns data in a background thread, so the callback to onResponse() is asynchronous to the UI, i.e. it takes some time until the data comes in; and therefore the getPlaylistItems() method will be returned before the retrofit data is up. And therefore it returns an empty list in val templist = getPlaylistItems().
To fix, this you can create a listener interface, or just build-up the RecyclerView within the onResponse callback:
override fun onResponse(call: Call<PlaylistRoot>, response: Response<PlaylistRoot>) {
if (response.code() == 200) {
var playlistinfo : PlaylistRoot = response.body();
playlisttitles.add(playlistinfo.title)
Log.e("PlaylistTitle!", "onResponseTitle: "+playlistinfo.title)
recyclerView.adapter = PlaylistRecyclerAdapter(playlisttitles.toTypedArray())
recyclerView.adapter?.notifyDataSetChanged()
}
}
Related
I am having an issue with reloading data in RecyclerView after the data is updated with a separate URL call. The FristFragment calls LoadDataFromUrl and displays the data in the Recycler View. Each Recycler's view item has a button with OnSetClickListener that calls another URL to update item's name data. After the user updates the item's name and a successful response is received, then the original myResponse data is updated with the new item's name. When the data is updated, I need to reload data in the Recycer View` but I can not figure it out. I spent two days trying to fix it but I can't get it running. Any help would be greatly appreciated!
Data Model:
DataModel.kt
class MyResponse (var Status: String = "", val items: List<Items> = emptyList())
class Items(var ItemId: String = "", var ItemName: String = "")
Code for Recycler View:
Adapter.kt
var recyclerView: RecyclerView? = null
class MainAdapter(val myResponse: MyResponse): RecyclerView.Adapter<CustomViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.my_custom_cell, parent,false)
return CustomViewHolder(cellForRow)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val item = myResponse.items.get(position)
//there is a button for each item in the list that will call a function ButtonPressed from Frist Fragment
holder.view.button.setOnClickListener {
val firstFragment = FirstFragment()
firstFragment.ButtonPressed(item.ItemId,item.ItemName, position)
}
holder.view.textView_ItemID = item.itemId
holder.view.textView_Item_Name = item.itemName
}
class CustomViewHolder(val view: View, var Items: Item? = null): RecyclerView.ViewHolder(view){
}
}
Then I have a fragment Fragment.kt
FirstFragment.kt
class FirstFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.first_fragment, container, false)
return rootView
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
LoadDataFromUrl()
recyclerView.layoutManager = LinearLayoutManager(this.context,
LinearLayoutManager.HORIZONTAL, false)
}
//this function loads data on create view
fun LoadDataFromUrl(){
val url = "some_url"
val payload = "some_data_to_send"
val requestBody = payload.toRequestBody()
val request = Request.Builder()
.method("POST",requestBody)
.url(url)
.build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("error")
}
override fun onResponse(call: Call, response: Response) {
val body = response?.body?.string()
val gson = GsonBuilder().create()
myResponse = gson.fromJson(body, MyResponse::class.java)
activity?.runOnUiThread {
if (myResponse.Status== "200"){
recyclerView.adapter = MainAdapter(myResponse)
}
}
}
})
}
fun ButtonPressed(itemId: String, itemName: String, position: Int){
val url = "some_another_url"
val payload = "some_another_data_to_send"
val requestBody = payload.toRequestBody()
val request = Request.Builder()
.method("POST",requestBody)
.url(url)
.build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("error")
}
override fun onResponse(call: Call, response: Response) {
val body = response?.body?.string()
val gson = GsonBuilder().create()
val buttomPressedResponse = gson.fromJson(body, ButtonPressedResponse::class.java)
if (buttonPressedResponse.Status== "200") {
myResponse.response[position].Status = buttomPressedResponse.Status //will update existing object myResponse with a new item's name
//this is where I have a problem
recyclerView.adapter.notifyDataSetChanged()
}
}
}
}
I tried the following changes but I still get an error.
//I get this error: Fatal Exception: OkHttp Dispatcher Process. recyclerView must not be null. Then the app crashes and the view reloads. If I clcik the Button again I get an error saying: RecyclerView: No adapter atatched. Skipping layout.
activity?.runOnUiThread {
myResponse.response[position].Status = checkInOutResponse.Status //will update existing object myResponse with updated data
recyclerView.adapter?.notifyDataSetChanged()
}
I also tried to run on it runOnUiTHread but nothing happens with this code
activity?.runOnUiThread {
myResponse.response[position].Status = checkInOutResponse.Status //will update existing object myResponse with updated data
recyclerView.adapter?.notifyDataSetChanged()
}
Create var myResponse: MyResponse variable in Adapter
Adapter.kt
var recyclerView: RecyclerView? = null
class MainAdapter(val myResponseInit: MyResponse): RecyclerView.Adapter<CustomViewHolder>(){
var myResponse: MyResponse
myResponse = myResponseInit
fun submitMyResponse(data: MyResponse) {
myResponse = data
}
//Rest of the code onCreateViewHolder etc.
}
Call submitMyResponse() function and notifyDataSetChanged() on adapter everytime you receive response.
I try to get data from json: https://github.com/rolling-scopes-school/rs.android.task.6/blob/master/data/data.json
Service to prepare retrofit to make the call:
object ServiceBuilder {
private val client = OkHttpClient.Builder().build()
var gson = GsonBuilder()
.setLenient()
.create()
private val retrofit = Retrofit.Builder()
.baseUrl("https://github.com/rolling-scopes-school/rs.android.task.6/blob/master/data/")
.addConverterFactory(GsonConverterFactory.create(gson))
.client(client)
.build()
fun<T> buildService(service: Class<T>): T{
return retrofit.create(service)
}
}
The Interface:
interface ApiInterface {
#GET("/data.json")
fun getItems(): Call <List<Item>>
}
MainActivity code:
val request = ServiceBuilder.buildService(ApiInterface::class.java)
val call = request.getItems()
call.enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Toast.makeText(this#MainActivity, "${t.message}", Toast.LENGTH_LONG).show()
}
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
if (response.isSuccessful){
recyclerview.apply {
progress_bar.visibility = View.GONE
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = MyAdapter(response.body()!!)
}
}
}
})
}
MyAdapter:
class MyAdapter(val items: List): RecyclerView.Adapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.recycler_layout, parent, false)
return ItemsViewHolder(view)
}
override fun getItemCount(): Int {
return items.size
System.out.println("items.size - " + items.size)
}
override fun onBindViewHolder(holder: ItemsViewHolder, position: Int) {
val title = items[position].title ?: ""
val description = items[position].description ?: ""
val imageurl = items[position].image.url ?: ""
holder.bind(title, description, imageurl)
}
}
class ItemsViewHolder(itemView : View): RecyclerView.ViewHolder(itemView){
private val photo: ImageView = itemView.findViewById(R.id.image_photo)
private val title_text: TextView = itemView.findViewById(R.id.title_)
private val description_text:TextView = itemView.findViewById(R.id.description)
fun bind(title: String,description:String,imageurl:String) {
Glide.with(itemView.context).load(imageurl).into(photo)
title_text.text = title
description_text.text = description
}
Dataclass Item:
data class Item(
val description: String,
val duration: Duration,
val enclosure: Enclosure,
val group: Group,
val guid: Guid,
val image: ImageX,
val link: String,
val pubDate: String,
val title: String
)
I run app and no such data in screen :-). What's wrong? Can you help me?
The problem seems to be lying here. you are getting the response and you are creating a new adapter each time, but not assigning the new adapter to the recyclerview i guess.
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
if (response.isSuccessful){
recyclerview.apply {
progress_bar.visibility = View.GONE
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = MyAdapter(response.body()!!)
//add this code to assign the adapter and notify the adapter
recyclerview.adapter = adapter
adapter.notifyDataSetChanged()
}
}
}
note: it would be better if you create your adapter in onCreate() and assign the adapter to RecyclerView there only, and now when you call the github api to get the details just update the data in the adapter and call notifyDataSetChanged on adapter to notify about the new data changed.
Edit: It seems there is issue with the api endpoint also you won't get the json data by hitting that url directly. you need to convert this to raw. something like this.
https://raw.githubusercontent.com/rolling-scopes-school/rs.android.task.6/master/data/data.json
Also the model which you have mentioned won't be mapped by json adapter because the response is in a different format so here change the model to something like below
data class GitResponse(val channel: Channel)
data class Channel(val item: List<Item>)
so change the retrofit with the following url param.
interface ApiInterface {
#GET
fun getItems(#Url url: String): Call<GitResponse>
}
//and where ever you are call this now pass the full url which is different from the one you have.
val url = "https://raw.githubusercontent.com/rolling-scopes-school/rs.android.task.6/master/data/data.json"
val request = ServiceBuilder.buildService(ApiInterface::class.java)
val call = request.getItems(url)
call.enqueue(object : Callback<GitResponse> {
override fun onFailure(call: Call<GitResponse>, t: Throwable) {
Toast.makeText(this#MainActivity, "${t.message}", Toast.LENGTH_LONG).show()
}
override fun onResponse(call: Call<GitResponse>, response: Response<GitResponse>) {
if (response.isSuccessful){
recyclerview.apply {
progress_bar.visibility = View.GONE
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = MyAdapter(response.body()?.channel?.item ?: listOf())
//add this code to assign the adapter and notify
recyclerview.adapter = adapter
adapter.notifyDataSetChanged()
}
}
}
})
now you will get the api response, check by applying debug point.
I am a beginner in Kotlin. I need to send a variable parameter from my Activity to a Retrofit call.
This is my call in on Create of Detail Activity
override fun onCreate(savedInstanceState: Bundle?) {
//...
val id = intent.getStringExtra("id")
// Get the ViewMode
val mModel = ViewModelProviders.of(this).get(myObjectViewModel::class.java)
//Create the observer which updates the UI.
val myObjectByIdObserver = Observer<MyObject> { myObject->
//...
}
//Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
mModel.getObjectById.observe(this, myObjectByIdObserver)
}
Here I insert value hardcode, I need the parameter received from the previous Activity.
class MyObjectViewModel : ViewModel() {
//this is the data that we will fetch asynchronously
var myObject: MutableLiveData<MyObject>? = null
val getMyObjectById: LiveData<MyObject>
get() {
if (myObject == null) {
myObject = MutableLiveData()
loadMyObjectById()
}
return myObject as MutableLiveData<MyObject>
}
private fun loadMyObjectById() {
val retrofit = Retrofit.Builder()
.baseUrl(Api.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(Api::class.java)
val call = api.myObjectById(100)
call.enqueue(object : Callback<MyObject> {
override fun onResponse(call: Call<MyObject>, response: Response<MyObject>) {
myObject!!.value = response.body()
}
override fun onFailure(call: Call<MyObject>, t: Throwable) {
var tt = t
}
})
}
My API:
interface Api {
companion object {
const val BASE_URL = "https://.../"
}
#GET("myObjects/{id}")
fun myObjectById(#Path("id") id: Int?): Call<MyObject>
}
You can do this by ``#Query``` annotation.
interface Api {
companion object {
const val BASE_URL = "https://.../"
}
#GET("myObjects/{id}")
fun myObjectById(#Path("id") id: Int?, #Query("a_param") aParam: String?): Call<MyObject>
}
Edited. I completely misread your intension.
What you need seems to be ViewModelProvider.NewInstanceFactory like
class MyObjectViewModel(val id: Int): ViewModel() {
class Factory(val id: Int) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyObjectViewModel(id) as T
}
}
}
then
val myViewModel = ViewModelProviders
.of(this, MyObjectViewModel.Factory(id))
.get(MyObjectViewModel::class.java)
I build a simple android application showing data from TMDb API using Retrofit, but how do I programmatically get data access speed when requesting data from the server and show it on Android studio Logcat?
class MainActivity : AppCompatActivity() {
lateinit var apiKey : String
var movies : MutableList<Movie> = mutableListOf()
var adapter = MovieAdapter(movies)
val movieService : MovieService = ApiClient.getClient()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
rvMovie.layoutManager = LinearLayoutManager(applicationContext)
rvMovie.adapter = adapter
apiKey = getString(R.string.api_key)
getPopularMovies(apiKey)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item)
}
fun getPopularMovies(apiKey: String) {
val call : Call<MovieResult> = movieService.getPopularMovies(apiKey)
getMovieData(call)
}
fun getMovieData(call : Call<MovieResult>) {
call.enqueue(object : Callback<MovieResult> {
override fun onFailure(call: Call<MovieResult>?, t: Throwable?) {
Toast.makeText(applicationContext, "${t.toString()}", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<MovieResult>?, response: Response<MovieResult>?) {
if (response?.body() != null) {
movies = response.body()!!.movies.toMutableList()
adapter = MovieAdapter(movies)
rvMovie.adapter = adapter
// HOW DO I MEASURE THE DATA SPEED TRANSFER
Log.i("speedtest", "Data transfer speed is = 99Kb/s");
}
}
})
}
Example
Try this
#Streaming // make you add this, since you are downloading movie
#Get("bla/bla/bla") // any Http method you are using
fun getPopularMovies(apiKey : Int)
...
// where you consume the apiservice
val call : Call<MovieResult> = movieService.getPopularMovies(apiKey)
getMovieData(call)
fun getMovieData(call : Call<MovieResult>){
call.enqueue(object : Callback<MovieResult> {
override fun onFailure(call: Call<MovieResult>?, t: Throwable?) {
Toast.makeText(applicationContext, "${t.toString()}", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<MovieResult>?, response: Response<MovieResult>?) {
if (response?.body() != null) {
val moviebytes = ByteArray(1024 * 1024 * 1024) // also try this val bytes = byteArrayOf() if does not work
val inputStream = body.byteStream();
val numberOfbytes = inputStream.read(moviebytes)
log(Log.i("speedtest", "Data transfer speed is = ${numberOfbytes}bytes/s");
}
}
})
}
So I am using the OpenWeatherMap Api to create a sun spotter app, so the issue I have right now is that, I have all the icons in my drawable folder, the issue is I want to display all the icons right now I have ti set up like this:
I am using that hard coded icon as a placeholder but I want all the other icons to show, my issue is how do I do that, the image from the api is given in string format and i don't know how I would get them from by drawable folder
Code
// Array Adapter
class ForecastAdapter(val forecast: Forecast) : RecyclerView.Adapter<ForecastData>(){
override fun getItemCount(): Int {
return forecast.list.count()
}
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ForecastData {
val layoutInflator = LayoutInflater.from(p0?.context)
val cellForRow = layoutInflator.inflate(R.layout.weather_row, p0, false)
return ForecastData(cellForRow)
}
override fun onBindViewHolder(p0: ForecastData, p1: Int) {
val getWeather = forecast.list[p1]
val clouds = getWeather.weather[0]
val getDateTime = forecast.list[p1]
val getIcon = forecast.list[p1]
// val icon = getIcon.weather[0]
p0.view.textView_text_clouds.text = clouds.main
p0.view.textView_date_time.text = getDateTime.dt_txt
// p0.view.imageView_icon = icon.icon
}
}
class ForecastData(val view: View): RecyclerView.ViewHolder(view){
}
// MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
searchButton.setOnClickListener {
getRequest()
}
view.layoutManager = LinearLayoutManager(this)
// val drawableId: Int = getResources().getIdentifier("drawable", "drawable", getPackageName())
}
private fun getRequest() {
val input = searchBar.getText().toString()
val url = "http://api.openweathermap.org/data/2.5/forecast?zip=" + input + "&units=imperial&APPID=" + getString(R.string.OPEN_WEATHER_MAP_API_KEY)
val request = okhttp3.Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call, response: okhttp3.Response) {
val body = response?.body()?.string()
println(body)
val gson = GsonBuilder().create()
val weather = gson.fromJson(body, Forecast::class.java)
runOnUiThread {
view.adapter = ForecastAdapter(weather)
}
}
override fun onFailure(call: Call, e: IOException) {
println("Failed to execute")
}
})
}
}
Use When
when("forecast.list[p1]") {
"icon01d.png" -> p0.view.imageView_icon.setDrawable(R.drawable.icon01d.png)
"icon02n.png" -> p0.view.imageView_icon.setDrawable(R.drawable.icon02n.png)
else -> // Your place holder icon for imageview
}