I'm a novice Android developer. I'm trying to build a movie database app using tmdb. For this, I am using the Kotlin language and chose to use Retrofit and GSON for my JSON parsing and HTTP calls. However, I haven't done this before. I went through multiple tutorials but one is different than the other and my A.D.D. doesn't really help when it comes to deriving the concepts.
For now, this is my code. All it does is take a placeholder image and display it in a 3 column recyclerview grid layout 300x (random number because I don't have a list size yet):
MainActivity.kt
class MainActivity() : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view);
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.layoutManager = GridLayoutManager(this, 3);
recyclerView.adapter = PosterAdapter()
}
}
PosterAdapter.kt
class PosterAdapter() : RecyclerView.Adapter<PosterHolder>(){
override fun getItemCount(): Int { return 300}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PosterHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val listItem = layoutInflater.inflate(R.layout.list_item, parent, false)
return PosterHolder(listItem)
}
override fun onBindViewHolder(holder: PosterHolder, position: Int) {
holder.view.movie_poster?.setImageResource(R.mipmap.beauty_and_the_beast_ver3)
holder.view.movie_poster?.scaleType = ImageView.ScaleType.FIT_XY
}
}
class PosterHolder(val view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
var imageView: ImageView? = null
fun PosterHolder(view: View){ this.imageView = view.findViewById<View>(R.id.movie_poster) as ImageView }
override fun onClick(p0: View?) {}
}
I don't want someone to cook the code for me. I can't learn that way. I would appreciate a simple step-by-step explanation on how to implement both libraries in my app.
Here is a simple code snippits for how you can setup Retrofit with Kotlin.
First, the api interface where you will define your network calls. For example:
interface ServerAPI {
#FormUrlEncoded
#POST("/api/v1/login")
fun login(#Header("Authorization") authorization: String): Call<LoginResponse>
}
Then, create Retrofit instance. Personally, I prefer to make a kotlin object and intialize it with by lazy. Also, notice the GsonConverterFactory
object RetrofitServer {
val client: ServerAPI by lazy {
val client = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.baseUrl("http://127.0.0.1/")
// For Gson
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
retrofit.create(ServerAPI::class.java)
}
}
Then that's it. To make a call, simply
RetrofitServer.client.login("Auth header").enqueue(object: Callback<BalanceResponse> {
override fun onFailure(call: Call<BalanceResponse>?, t: Throwable?) {
}
override fun onResponse(call: Call<BalanceResponse>?, t: Response<BalanceResponse>?) {
}
}
)
Related
I try to make a recycler view with the name and picture of the pokemon, but it doesn't work, I don't understand why, the requests are not done because the pokemon list is never filled. I make a loop for calls because what I want is the name and picture of the pokemon, because when I ask for a list of pokemon objects only bring the name and the url of the complete object and is the best way that has occurred to me, I would appreciate if you can give me a hand, thank you very much.
This is the code of my mainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initRecycler()
obtenerPokemons()
}
private fun initRecycler(){
pokemonAdapter = PokemonAdapter(pokemons)
linearLayoutManager = LinearLayoutManager(this)
binding.recyclerpokemon.apply {
setHasFixedSize(true)
layoutManager = linearLayoutManager
adapter = pokemonAdapter
}
}
private fun obtenerPokemons() {
for (i in 1..30) {
searchByName(i)
}
pokemonAdapter.setPokemons(pokemons)
}
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private fun searchByName(query:Int){
CoroutineScope(Dispatchers.IO).launch {
val call = getRetrofit().create(PokemonService::class.java).getPokemon("pokemon/$query")
val pokemonsResp = call.body()
runOnUiThread {
if(call.isSuccessful) {
pokemons.add(pokemonsResp)
}else{
Toast.makeText(this#MainActivity, "No encuentro eso", Toast.LENGTH_SHORT).show()
}
}
}
}
}
This is the pokemon object code:
data class Pokemon (
#SerializedName("id" ) var id : Int,
#SerializedName("name" ) var name : String,
#SerializedName("sprites" ) var sprites : Sprites,
)
data class Sprites (
#SerializedName("back_default" ) var backDefault : String,
#SerializedName("front_default" ) var frontDefault : String,
)
And this is the code of my service:
interface PokemonService {
#GET
suspend fun getPokemon(#Url url:String) : Response<Pokemon?>
}
you need to call pokemonAdapter.notifyDataSetChanged() after pokemons.add(pokemonsResp). notifyDataSetChanged() will update the recycler view with the new data.
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()
}
}
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 implementing a RecyclerView in a fragment. The XML should be correct since I tried it with my hard-coded data, and the API call does return the correct json data from the server according to the Log in the console. The problem is that the RecyclerView adapter does not get any data from my Observable. Here is my implementation
In PostDataService interface I used Retrofit to get an Observable>
interface PostDataService {
#GET(".")
fun getPosts(
#Query(value = "offset") offset: Long = 0,
#Query(value = "limit") limit: Long = 10,
#Query(value = "subscribedOnly") subscribedOnly: Boolean = false
): Observable<List<Post>>
companion object {
val retrofit: PostDataService = Retrofit.Builder()
.baseUrl("http:aws/api/post/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
.create(PostDataService::class.java)
}
}
In PostListRepository, I used RxJava operators to get the LiveData
class PostListRepository {
private val postListLiveData: MutableLiveData<List<Post>> = MutableLiveData()
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
fun getPostListLiveData(): MutableLiveData<List<Post>> {
val postList: MutableList<Post> = ArrayList()
val retrofitInstance = PostDataService.retrofit
val postListObservable = retrofitInstance.getPosts()
compositeDisposable.add(
postListObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMapIterable { it }
.subscribeWith(object : DisposableObserver<Post>() {
override fun onError(e: Throwable) {
// if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(post: Post) {
postList.add(post)
}
override fun onComplete() {
postListLiveData.postValue(postList)
}
})
)
return postListLiveData
}
fun clear() {
compositeDisposable.clear()
}
}
In PostListViewModel, I passed the LiveData from the repository into this ViewModel.
class PostListViewModel : ViewModel() {
private var postListRepository: PostListRepository = PostListRepository()
fun getPostList(): MutableLiveData<List<Post>> {
return postListRepository.getPostListLiveData()
}
fun clear() {
postListRepository.clear()
}
}
Here is the Fragment that contains the RecyclerView. I think the .oberserve function in getPostList() is not called since I tried Log it but got nothing.
class PostListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var swipeLayout: SwipeRefreshLayout
private lateinit var postListViewModel: PostListViewModel
private val postListAdapter = PostRecyclerViewAdapter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.view_post_list, container, false)
recyclerView = rootView.findViewById(R.id.postRecyclerView)
recyclerView.apply {
setHasFixedSize(true)
addItemDecoration(VerticalSpaceItemDecoration(36))
layoutManager = LinearLayoutManager(context)
adapter = postListAdapter
}
postListViewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
getPostList()
swipeLayout = rootView.findViewById(R.id.swipeLayout)
swipeLayout.setColorSchemeResources(R.color.colorPrimary)
swipeLayout.setOnRefreshListener {
getPostList()
swipeLayout.isRefreshing = false
}
return rootView
}
override fun onDestroy() {
super.onDestroy()
postListViewModel.clear() // to avoid memory leak
}
private fun getPostList() {
postListViewModel.getPostList().observe(this, Observer<List<Post>> { resource ->
postListAdapter.setPostList(resource)
postListAdapter.notifyDataSetChanged()
})
}
}
Here is the adapter for the RecyclerView:
class PostRecyclerViewAdapter : RecyclerView.Adapter<PostViewHolder>() {
private var postList: List<Post> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
// create a new view
val postView = PostView(parent.context)
// set the view's size, margins, paddings and layout parameters
return PostViewHolder.from(postView)
}
override fun getItemCount(): Int = postList.size
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val curPost = postList[position]
holder.postView.apply {
setPostOwnerDisplayName(curPost.content.userDisplayedName)
setPostOwnerRole(curPost.content.role)
setPostOwnerAvatar(R.mipmap.ic_launcher_round)
setPostText(curPost.content.text)
setPostImage(curPost.content.smallMediaPaths[0])
setLikeState(curPost.liked)
setBookmarkState(curPost.bookmarked)
}
}
fun setPostList(postList: List<Post>) {
this.postList = postList
}
}
As I mentioned above, I think the .oberserve function in getPostList() in PostListFragment is not called since I tried Log it but got nothing, so there is no data passed into the RecyclerView. Can anyone help me find the reason why it's not being called, or why it's not getting the data from the ViewModel?
I wouldn't think of this is related to your issue, but your code has potential problems.
To move observe part to onActivityCreated would be better to ensure view is created.
when your fragment view is re-created, a new Observer will be added, while previous one still alive, because your Observer is anonymous. So, you have to manage the observers to prevent it.
I just found out that I forgot to catch the exception in RxJava onNext() in case to get the moshi serialization error. After getting that, I got some moshi conversion errors.
Posted it in case anyone carelessly forgot to catch the moshi error.
Thanks!
I want to fetch some json data, see in the image the green arrow:
The problem is that Android Studio doesn't let me get the data I want. It stops until a step before (I think). In my adapter class check:
holder?.view?.textWeather?.text = weatherFor.weather.toString()
Also it shows me in the emulator the red arrow, what is this?
Below is my main Activity's json method with the classes i want to fetch data for, and the associated Adapter class.
Main Activity
fun fetchJson() {
val url="https://api.openweathermap.org/data/2.5/forecast?q=Prague,CZ&appid=4cf7f6610d941a1ca7583f50e7e41ba3"
val request=Request.Builder().url(url).build()
val client= OkHttpClient()
client.newCall(request).enqueue(object :Callback {
override fun onResponse(call: Call?, response: Response?) {
val body=response?.body()?.string()
println(body)
val gson=GsonBuilder().create()
val forecastfeed=gson.fromJson(body,ForecastFeed::class.java)
runOnUiThread{
recyclerView_main.adapter=MainAdapter(forecastfeed)
}
}
override fun onFailure(call: Call?, e: IOException?) {
println("Failed to execute request")
}
})
}
class ForecastFeed(val list:List<ForecastWeatherList>) { }
class ForecastWeatherList(val weather:List<WeatherData>) { }
class WeatherData(val main:String,val icon:String) { }
Adapter
class MainAdapter(val forecastfeed: ForecastFeed): RecyclerView.Adapter<CustomViewHolder>() {
val forecastWeather = listOf<String>("First","Second")
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val weatherFor = forecastfeed.list.get(position)
holder?.view?.textWeather?.text = weatherFor.weather.toString()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder{
//how do we even create a view
val layoutInflater =LayoutInflater.from(parent?.context)
val cellForRow=layoutInflater.inflate(R.layout.weather_row,parent,false)
return CustomViewHolder(cellForRow)
}
override fun getItemCount(): Int {
return forecastfeed.list.count()
}
}
class CustomViewHolder(val view: View):RecyclerView.ViewHolder(view) { }
You can format the data manually
holder?.view?.textWeather?.text = "weather ${weatherFor.weather.map{it.main}.joinToString(", ")}"
or use data classes
You need to overwrite WeatherData.toString() to have a hand on what's displayed.
class WeatherData(val main:String,val icon:String) {
override fun toString(): String {
return "$main $icon"
}
}
Further more you should use a RecyclerView with a ViewHolder to handle properties one-by-one and enable more complex layouts. If needed.