I'm trying to build an app with Android Architecture Components. I'm using TMDB API in my app. In my app, a user searches for a movie or series and gets the result. I've achieved this but I want to get all the pages from API with Paging library. (Endless Recyclerview) I've looked at several tutorials but I didn't get what I wanted. Please help me, I'm new with this Android Architecture Components. Thank you in advance.
The API result:
RecyclerViewMovieAdapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.martiandeveloper.muuvi.R
import com.martiandeveloper.muuvi.model.Movie
import kotlinx.android.synthetic.main.recyclerview_movie_item.view.*
class RecyclerViewMovieAdapter(private val movieList: ArrayList<Movie>) :
RecyclerView.Adapter<RecyclerViewMovieAdapter.RecyclerViewMovieViewHolder>() {
lateinit var context: Context
class RecyclerViewMovieViewHolder(var view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerViewMovieViewHolder {
context = parent.context
val view = LayoutInflater.from(context)
.inflate(R.layout.recyclerview_movie_item, parent, false)
return RecyclerViewMovieViewHolder(view)
}
override fun getItemCount(): Int {
return movieList.size
}
override fun onBindViewHolder(holder: RecyclerViewMovieViewHolder, position: Int) {
if (movieList[position].mediaType != "person") {
if (movieList[position].mediaType == "tv") {
val title =
movieList[position].originalName + " (" + movieList[position].firstAirDate?.split(
"-"
)?.get(0) + ")"
holder.view.recyclerview_movie_item_titleMTV.text = title
} else {
val title =
movieList[position].title + " (" + movieList[position].releaseDate?.split("-")
?.get(0) + ")"
holder.view.recyclerview_movie_item_titleMTV.text = title
}
holder.view.recyclerview_movie_item_voteAverageMTV.text =
movieList[position].voteAverage.toString()
Glide.with(context)
.load("https://image.tmdb.org/t/p/w300${movieList[position].posterPath}")
.placeholder(R.drawable.logo1)
.centerCrop()
.into(holder.view.recyclerview_movie_item_posterIV)
}
}
fun updateMovieList(newMovieList: List<Movie>) {
movieList.clear()
movieList.addAll(newMovieList)
notifyDataSetChanged()
}
}
Movie.kt
import com.google.gson.annotations.SerializedName
data class Movie(
#SerializedName("original_name")
val originalName: String?,
#SerializedName("genre_ids")
val genreIds: List<Int>?,
#SerializedName("media_type")
val mediaType: String?,
#SerializedName("name")
val name: String?,
#SerializedName("origin_country")
val originCountry: List<String>?,
#SerializedName("first_air_date")
val firstAirDate: String?,
#SerializedName("original_language")
val originalLanguage: String?,
#SerializedName("id")
val id: Int?,
#SerializedName("vote_average")
val voteAverage: Float?,
#SerializedName("overview")
val overview: String?,
#SerializedName("poster_path")
val posterPath: String?,
#SerializedName("title")
val title: String?,
#SerializedName("release_date")
val releaseDate: String?,
#SerializedName("original_title")
val originalTitle: String?)
MovieResult.kt
import com.google.gson.annotations.SerializedName
data class MovieResult(
#SerializedName("page")
val page: Int?,
#SerializedName("total_results")
val totalResults: Int?,
#SerializedName("total_pages")
val totalPages: Int?,
#SerializedName("results")
val results: ArrayList<Movie>?
)
MovieApi.kt
import com.martiandeveloper.muuvi.model.MovieResult
import io.reactivex.Single
import retrofit2.http.GET
import retrofit2.http.Query
interface MovieApi {
#GET("search/multi")
fun getMovie(
#Query("api_key") apiKey: String,
#Query("query") movie: String,
#Query("page") page: Int
): Single<MovieResult>
}
MovieService.kt
import com.martiandeveloper.muuvi.model.MovieResult
import io.reactivex.Single
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
const val API_KEY = "fb640042b4bc08f6f7f65fbd8573f2a9"
const val BASE_URL = "https://api.themoviedb.org/3/"
// https://api.themoviedb.org/3/search/multi?api_key=my_api_key&query=break&page=1
// https://image.tmdb.org/t/p/w342/or06FN3Dka5tukK1e9sl16pB3iy.jpg
class MovieService {
private val api =
Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
.create(MovieApi::class.java)
fun getData(movie: String, page: Int): Single<MovieResult> {
return api.getMovie(API_KEY, movie, page)
}
}
AddFragment.kt
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.martiandeveloper.muuvi.R
import com.martiandeveloper.muuvi.adapter.RecyclerViewMovieAdapter
import com.martiandeveloper.muuvi.databinding.FragmentAddBinding
import com.martiandeveloper.muuvi.viewmodel.AddViewModel
import kotlinx.android.synthetic.main.fragment_add.*
class AddFragment : Fragment(), View.OnClickListener {
private lateinit var vm: AddViewModel
private val adapter = RecyclerViewMovieAdapter(arrayListOf())
private lateinit var binding: FragmentAddBinding
private lateinit var layoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = activity?.run {
ViewModelProviders.of(this)[AddViewModel::class.java]
} ?: throw Exception("Invalid activity")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_add, container, false)
binding.addViewModel = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
private fun initUI() {
setRecyclerView()
observe()
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
binding.isClearIVGone = true
setListeners()
fragment_add_movieSeriesET.requestFocus()
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(fragment_add_movieSeriesET, InputMethodManager.SHOW_IMPLICIT)
}
private fun setRecyclerView() {
layoutManager = LinearLayoutManager(context)
fragment_add_mainRV.layoutManager = layoutManager
fragment_add_mainRV.adapter = adapter
}
private fun observe() {
vm.movieList.observe(viewLifecycleOwner, Observer { movieList ->
movieList?.let {
adapter.updateMovieList(it)
}
})
vm.isError.observe(viewLifecycleOwner, Observer { isError ->
isError?.let {
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
if (it) {
setToast(resources.getString(R.string.something_went_wrong_please_try_again_later))
}
}
})
vm.isLoading.observe(viewLifecycleOwner, Observer { isLoading ->
isLoading?.let {
if (it) {
setProgress(isRecyclerViewGone = true, isProgressLLViewGone = false)
} else {
setProgress(isRecyclerViewGone = false, isProgressLLViewGone = true)
}
}
})
vm.movieSeriesETContent.observe(viewLifecycleOwner, Observer {
if (it.isNotEmpty()) {
vm.refreshData(it, 1)
binding.isClearIVGone = false
} else {
adapter.updateMovieList(arrayListOf())
binding.isClearIVGone = true
}
})
}
private fun setProgress(isRecyclerViewGone: Boolean, isProgressLLViewGone: Boolean) {
if (vm.movieSeriesETContent.value != null) {
val text =
"${resources.getString(R.string.searching_for)} \"${vm.movieSeriesETContent.value}\"..."
binding.searchingFor = text
}
binding.isMainRVGone = isRecyclerViewGone
binding.isProgressLLGone = isProgressLLViewGone
}
private fun setToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
private fun setListeners() {
fragment_add_clearIV.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (v != null) {
when (v.id) {
R.id.fragment_add_clearIV -> fragment_add_movieSeriesET.text.clear()
}
}
}
}
AddViewModel.kt
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.martiandeveloper.muuvi.model.MovieResult
import com.martiandeveloper.muuvi.model.Movie
import com.martiandeveloper.muuvi.service.MovieService
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observers.DisposableSingleObserver
import io.reactivex.schedulers.Schedulers
class AddViewModel : ViewModel() {
private val movieService = MovieService()
private val disposable = CompositeDisposable()
val movieList = MutableLiveData<ArrayList<Movie>>()
val isError = MutableLiveData<Boolean>()
val isLoading = MutableLiveData<Boolean>()
val movieSeriesETContent = MutableLiveData<String>()
fun refreshData(movie: String, page: Int) {
isLoading.value = true
disposable.add(
movieService.getData(movie, page).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<MovieResult>() {
override fun onSuccess(t: MovieResult) {
movieList.value = t.results
isError.value = false
isLoading.value = false
}
override fun onError(e: Throwable) {
isError.value = true
isLoading.value = false
}
})
)
}
}
Related
I'm retrieving a list of lists from an API in JSON. This is how the response looks like.
{
"result":"success",
"documentation":"https://www.exchangerate-api.com/docs",
"terms_of_use":"https://www.exchangerate-api.com/terms"
"supported_codes":[
["AED","UAE Dirham"],
["AFN","Afghan Afghani"],
["ALL","Albanian Lek"],
["AMD","Armenian Dram"],
["ANG","Netherlands Antillian Guilder"],
["AOA","Angolan Kwanza"],
["ARS","Argentine Peso"],
["AUD","Australian Dollar"],
["AWG","Aruban Florin"],
["AZN","Azerbaijani Manat"],
["BAM","Bosnia and Herzegovina Convertible Mark"],
["BBD","Barbados Dollar"] etc. etc.
]
}
And this is the data class
data class CurrencyResponse(
#Json(name="documentation") val documentation: String,
#Json(name="result") val result: String,
#Json(name="supported_codes") val supported_codes: List<List<String>>,
#Json(name="terms_of_use") val terms_of_use: String
)
I managed to get the list of lists(supported_codes) but what I want now is to get the first index of each list(AED,AFN,ALL,etc.) and store them in a list, convert it to an array, then use that array to populate my spinners. Can anyone help me out with that?
Below is the relevant code.
Thank you.
CurrencyRepository.kt
package com.example.currencyconverter.data
import com.example.currencyconverter.BuildConfig
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class CurrencyRepository #Inject constructor(private val currencyApi: CurrencyApi) {
suspend fun getConversionRate(baseCurrency: String, toCurrency: String, amount: Double?): ConversionResponse {
return currencyApi.convert(BuildConfig.API_KEY, baseCurrency, toCurrency, amount)
}
suspend fun getCurrencies(): List<String> {
return currencyApi.getCurrencies(BuildConfig.API_KEY).supported_codes
}
}
CurrencyViewModel.kt
package com.example.currencyconverter.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.currencyconverter.data.CurrencyRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class CurrencyViewModel #Inject constructor(private val repository: CurrencyRepository): ViewModel() {
private val _conversionResult = MutableLiveData<String>()
val conversionResult: LiveData<String> = _conversionResult
private val _currencies = MutableLiveData<List<String>>()
val currencies: LiveData<List<String>> = _currencies
init {
getCurrencies()
}
fun getConversionRate(baseCurrency: String, toCurrency: String, amount: Double?) {
viewModelScope.launch {
_conversionResult.value = repository.getConversionRate(baseCurrency, toCurrency, amount).conversion_result
}
}
fun getCurrencies() {
viewModelScope.launch {
_currencies.value = repository.getCurrencies()
}
}
}
HomeFragment.kt
package com.example.currencyconverter.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.currencyconverter.R
import com.example.currencyconverter.databinding.FragmentHomeBinding
import dagger.hilt.android.AndroidEntryPoint
#AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private val viewModel by viewModels<CurrencyViewModel>()
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
lateinit var firstCurrency: String
lateinit var secondCurrency: String
viewModel.currencies.observe(viewLifecycleOwner) {
binding.list.text = " ${it[0]} , ${it[1]} "
}
var currenciesFormat = arrayOf(viewModel.currencies.value?.get(0),
viewModel.currencies.value?.get(1)
)
binding.apply {
spinnerFirst.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, currenciesFormat)
spinnerFirst.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
firstCurrency = adapterView?.getItemAtPosition(position).toString()
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
spinnerSecond.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
secondCurrency = adapterView?.getItemAtPosition(position).toString()
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
}
binding.constraintLayout.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
closeKeyboard(p0)
return true
}
})
binding.button.setOnClickListener {
val stringInTextField = binding.amountText.text.toString()
val amount = stringInTextField.toDoubleOrNull()
if (amount == null) {
binding.resultText.text = " "
}
closeKeyboard(binding.amountText)
viewModel.getConversionRate(firstCurrency, secondCurrency, amount)
viewModel.conversionResult.observe(viewLifecycleOwner) {
binding.resultText.text = it
}
}
}
private fun closeKeyboard(view: View?) {
val imm =
view?.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (view != null) {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}
}
You can use kotlin.collections's map function. https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/map.html
fun main(){
val supportedCodes = listOf(
listOf("AED","UAE Dirham"),
listOf("AFN","Afghan Afghani"),
listOf("ALL","Albanian Lek")
)
val codes = supportedCodes.map { it[0] }
println(codes)
}
The output is a list containing just the first element from each nested list.
[AED, AFN, ALL]
This project is a recycler view fragment that gets images from flickr and display it.
I used loopers and handlers to communicate between the recycler fragment and the downloading thumbnail fragment... I'm using Android Studio Bumblebee...
I’m facing an error with requestHandler… Maybe i’m not seeing a specific issue. but i checked the code several times but couldn’t find the missing part…
I get this error:
kotlin.UninitializedPropertyAccessException: lateinit property requestHandler has not been initialized at com.bignerdranch.android.photogallery.ThumbnailDownloader.queueThumbnail(ThumbnailDownloader.kt:94)
it is referring to this one…
fun queueThumbnail(target: T, url: String){ //page 510
Log.i(TAG, "Got a URL: $url")
requestMap[target] = url //page 521
requestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
.sendToTarget()
}
this code is being called by the Recyclerview from another fragment
override fun onBindViewHolder(holder: PhotoHolder, position: Int) {
val galleryItem = galleryItems[position]
//holder.bindTitle(galleryItem.title)
val placeholder: Drawable = ContextCompat.getDrawable(
requireContext(),
R.drawable.hala_atamleh
)?: ColorDrawable()
holder.bindTitle(placeholder) //should be bindDrawable but it didn't work
--> thumbnailDownloader.queueThumbnail(holder, galleryItem.url) //page 515
}
as far as i know, i Initialized it here:
#Suppress("UNCHECKED_CAST") //page 522
#SuppressLint("HandlerLeak")
override fun onLooperPrepared() {
requestHandler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
if (msg.what == MESSAGE_DOWNLOAD){
val target = msg.obj as T
Log.i(TAG, "Got a request for URL: ${requestMap[target]}")
handleRequest(target)
}
}
}
}
what did i missed to get this error?
here is the file code:
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import java.util.concurrent.ConcurrentHashMap
private const val TAG = "ThumbnailDownloader"
private const val MESSAGE_DOWNLOAD = 0
class ThumbnailDownloader<in T>(private val responseHandler: Handler, //page. 524
private val onThumbnailDownloaded : (T, Bitmap) -> Unit)
: HandlerThread(TAG) /*, LifecycleObserver page 512*/ { //page. 510
private lateinit var requestHandler: Handler //page516++
private var hasQuit = false
private val requestMap = ConcurrentHashMap<T, String>()
private val flickrFetchr = FlickrFetchr()
val fragmentLifecycleObserver: LifecycleObserver = //page 527
object : LifecycleObserver{
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun setup() {
Log.i(TAG, "Starting background thread")
start() //page 514
looper
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun tearDown() {
Log.i(TAG, "Destroying background thread")
quit()
}
}
#Suppress("UNCHECKED_CAST") //page 522
#SuppressLint("HandlerLeak")
override fun onLooperPrepared() {
requestHandler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
if (msg.what == MESSAGE_DOWNLOAD){
val target = msg.obj as T
Log.i(TAG, "Got a request for URL: ${requestMap[target]}")
handleRequest(target)
}
}
}
}
//page 528
val viewLifecycleObserver : LifecycleObserver =
object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun tearDown(){
Log.i(TAG, "Clearing all requests from Queue")
requestHandler.removeMessages(MESSAGE_DOWNLOAD)
requestMap.clear()
}
}
override fun quit(): Boolean {
hasQuit = true
return super.quit()
}
fun queueThumbnail(target: T, url: String){ //page 510
Log.i(TAG, "Got a URL: $url")
requestMap[target] = url //page 521
requestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
.sendToTarget()
}
fun clearQueue() {
requestHandler.removeMessages(MESSAGE_DOWNLOAD)
requestMap.clear()
}
private fun handleRequest(target: T){
val url = requestMap[target] ?: return
val bitmap = flickrFetchr.fetchPhoto(url) ?: return
responseHandler.post(Runnable { //page.526
if(requestMap[target] != url || hasQuit){
return#Runnable
}
requestMap.remove(target)
onThumbnailDownloaded(target, bitmap)
})
}
}
PHOTOGalleryFragment:
package com.bignerdranch.android.photogallery
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
private lateinit var photoRecyclerView : RecyclerView
private lateinit var photoGalleryViewModel: PhotoGalleryViewModel
private lateinit var thumbnailDownloader: ThumbnailDownloader<PhotoGalleryFragment.PhotoHolder> //it should be PhotoHolder not Type or Handler
private const val TAG = "PhotoGalleryFragment"
class PhotoGalleryFragment:Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true //page 511
photoGalleryViewModel = //page 494
ViewModelProvider(this).get(PhotoGalleryViewModel::class.java)
//thumbnailDownloader = ThumbnailDownloader() //p.513
val responseHandler = Handler(Looper.getMainLooper())//page 525
thumbnailDownloader =
ThumbnailDownloader(responseHandler){ photoHolder, bitmap ->
val drawable = BitmapDrawable(resources, bitmap)
photoHolder.bindDrawable(drawable)
}
lifecycle.addObserver(thumbnailDownloader.viewLifecycleObserver)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewLifecycleOwner.lifecycle.addObserver( //page. 529
thumbnailDownloader.viewLifecycleObserver
)
val view = inflater.inflate(R.layout.fragment_photo_gallery, container, false)
photoRecyclerView = view.findViewById(R.id.photo_recycler_view)
photoRecyclerView.layoutManager = GridLayoutManager(context,3)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
photoGalleryViewModel.galleryItemLiveData.observe(
viewLifecycleOwner,
Observer { galleryItems ->
Log.d(TAG,"Have gallery items from ViewModel $galleryItems" )
photoRecyclerView.adapter = PhotoAdapter(galleryItems)
}
)
}
class PhotoHolder(private val itemImageView: ImageView)
: RecyclerView.ViewHolder(itemImageView){ //page 496
val bindDrawable: (Drawable) -> Unit = itemImageView::setImageDrawable
}
private inner class PhotoAdapter(private val galleryItems: List<GalleryItem>)
:RecyclerView.Adapter<PhotoHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoHolder {
val view = layoutInflater.inflate(
R.layout.list_item_gallery,
parent,
false) as ImageView
return PhotoHolder(view)
}
override fun onBindViewHolder(holder: PhotoHolder, position: Int) {
val galleryItem = galleryItems[position]
//holder.bindTitle(galleryItem.title)
val placeholder: Drawable = ContextCompat.getDrawable(
requireContext(),
R.drawable.hala_atamleh
)?: ColorDrawable()
holder.bindDrawable(placeholder)
thumbnailDownloader.queueThumbnail(holder, galleryItem.url) //page 515
}
override fun getItemCount(): Int = galleryItems.size
}
companion object{
fun newInstance() = PhotoGalleryFragment()
}
override fun onDestroyView() { //page 530
super.onDestroyView()
thumbnailDownloader.clearQueue()
viewLifecycleOwner.lifecycle.removeObserver(
thumbnailDownloader.viewLifecycleObserver
)
}
override fun onDestroy() {
super.onDestroy()
lifecycle.removeObserver(
thumbnailDownloader.fragmentLifecycleObserver
)
}
}
I solved it in 2 steps
1- made requesthandler nullable... thanks to #Tenfour04
2- made the gallery this way
package com.bignerdranch.android.photogallery
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
private const val TAG = "PhotoGalleryFragment"
class PhotoGalleryFragment : Fragment() {
private lateinit var photoGalleryViewModel: PhotoGalleryViewModel
private lateinit var photoRecyclerView: RecyclerView
private lateinit var thumbnailDownloader: ThumbnailDownloader<PhotoHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
photoGalleryViewModel =
ViewModelProviders.of(this).get(PhotoGalleryViewModel::class.java)
val responseHandler = Handler()
thumbnailDownloader =
ThumbnailDownloader(responseHandler) { photoHolder, bitmap ->
val drawable = BitmapDrawable(resources, bitmap)
photoHolder.bindDrawable(drawable)
}
lifecycle.addObserver(thumbnailDownloader.fragmentLifecycleObserver)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewLifecycleOwner.lifecycle.addObserver(
thumbnailDownloader.viewLifecycleObserver
)
val view = inflater.inflate(R.layout.fragment_photo_gallery, container, false)
photoRecyclerView = view.findViewById(R.id.photo_recycler_view)
photoRecyclerView.layoutManager = GridLayoutManager(context, 3)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
photoGalleryViewModel.galleryItemLiveData.observe(
viewLifecycleOwner,
Observer { galleryItems ->
Log.d(TAG, "Have gallery items from view model $galleryItems")
photoRecyclerView.adapter = PhotoAdapter(galleryItems)
})
}
override fun onDestroyView() {
super.onDestroyView()
thumbnailDownloader.clearQueue()
viewLifecycleOwner.lifecycle.removeObserver(
thumbnailDownloader.viewLifecycleObserver
)
}
override fun onDestroy() {
super.onDestroy()
lifecycle.removeObserver(
thumbnailDownloader.fragmentLifecycleObserver
)
}
private class PhotoHolder(private val itemImageView: ImageView) : RecyclerView.ViewHolder(itemImageView) {
val bindDrawable: (Drawable) -> Unit = itemImageView::setImageDrawable
}
private inner class PhotoAdapter(private val galleryItems: List<GalleryItem>) :
RecyclerView.Adapter<PhotoHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PhotoHolder {
val view = layoutInflater.inflate(
R.layout.list_item_gallery,
parent,
false
) as ImageView
return PhotoHolder(view)
}
override fun getItemCount(): Int = galleryItems.size
override fun onBindViewHolder(holder: PhotoHolder, position: Int) {
val galleryItem = galleryItems[position]
val placeholder: Drawable = ContextCompat.getDrawable(
requireContext(),
R.drawable.bill_up_close
) ?: ColorDrawable()
holder.bindDrawable(placeholder)
thumbnailDownloader.queueThumbnail(holder, galleryItem.url)
}
}
companion object {
fun newInstance() = PhotoGalleryFragment()
}
}
Just after calling loadInitial method it automatically starts calling loadAfter method until and unless all pages are loaded and it gets an empty array from response. I am attaching my DataSource and DataSourceFactory code along with ViewModel function and PagedListAdapter.
PassbookDataSource.kt
package co.indiagold.gold.buy.loan.home
import androidx.paging.PageKeyedDataSource
import co.indiagold.gold.buy.loan.helper.GenericMethods.handleServerError
import co.indiagold.gold.buy.loan.helper.GenericResponse
import co.indiagold.gold.buy.loan.network.Repository
import co.indiagold.gold.buy.loan.network.ResultWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class PassbookDataSource(private val accessToken: String, private val assetName:String,private val viewModelScope: CoroutineScope) : PageKeyedDataSource<Int, PassbookResponseModel>() {
private val PAGE_SIZE = 20
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, PassbookResponseModel>) {
var result: ResultWrapper<GenericResponse<ArrayList<PassbookResponseModel>?>?>? = null
viewModelScope.launch {
result = Repository.getPassbookDetail(accessToken, assetName,1, PAGE_SIZE)
}.invokeOnCompletion {
if (result is ResultWrapper.Success) {
val successResult = result as ResultWrapper.Success
callback.onResult(successResult.value!!.result as MutableList<PassbookResponseModel>, null, 2)
} else {
it.handleServerError("vault/passbook")
}
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, PassbookResponseModel>) {
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PassbookResponseModel>) {
var result: ResultWrapper<GenericResponse<ArrayList<PassbookResponseModel>?>?>? = null
viewModelScope.launch {
result = Repository.getPassbookDetail(accessToken,assetName, params.key, PAGE_SIZE)
}.invokeOnCompletion {
if (result is ResultWrapper.Success) {
val successResult = result as ResultWrapper.Success
callback.onResult(successResult.value!!.result as MutableList<PassbookResponseModel>, params.key+1)
} else {
it.handleServerError("vault/passbook")
}
}
}
}
PassbookDataSourceFactory.kt
class PassbookDataSourceFactory(private val accessToken:String,private val assetName:String,private val viewModelScope:CoroutineScope) : DataSource.Factory<Int,PassbookResponseModel>(){
val orderListMLD = MutableLiveData<PassbookDataSource>(null)
var orderDataSource : PassbookDataSource? = null
override fun create(): DataSource<Int, PassbookResponseModel> {
orderDataSource = PassbookDataSource(accessToken,assetName,viewModelScope)
orderListMLD.postValue(orderDataSource)
return orderDataSource!!
}
}
Viewmodel Function
fun getPassbookDetail(assetName: String) {
val accessToken = GenericMethods.getDataFromPreferences(Constants.SHARED_PREFS.LOGIN_PREFERENCES, Constants.SHARED_PREFS.ACCESS_TOKEN)
if (accessToken.isNullOrEmpty()) {
handleNullAccessTokenCase()
} else {
val factory = PassbookDataSourceFactory(accessToken, assetName, viewModelScope)
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(10)
.setPrefetchDistance(4)
.build()
val executor = Executors.newFixedThreadPool(5)
passbookResponseMLD = LivePagedListBuilder(factory, config)
.setFetchExecutor(executor)
.build()
}
}
PassbookAdapter.kt
package co.indiagold.gold.buy.loan.home
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import co.indiagold.gold.buy.loan.databinding.CardPassbookBinding
import co.indiagold.gold.buy.loan.helper.Constants
import co.indiagold.gold.buy.loan.orders.OrdersActivity
import kotlinx.android.synthetic.main.card_passbook.view.*
class PassbookAdapter(private val activity: Activity,private val emptyListListener: EmptyListListener) : PagedListAdapter<PassbookResponseModel,PassbookAdapter.MyHolder>(PASSBOOK_DIFF_CALLBACK) {
private lateinit var binding: CardPassbookBinding
class MyHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
binding = CardPassbookBinding.inflate(LayoutInflater.from(parent.context))
emptyListListener.onListLoad(currentList?.size)
return MyHolder(binding.root)
}
override fun onCurrentListChanged(previousList: PagedList<PassbookResponseModel>?, currentList: PagedList<PassbookResponseModel>?) {
super.onCurrentListChanged(previousList, currentList)
emptyListListener.onListLoad(currentList?.size)
}
override fun onBindViewHolder(holder: MyHolder, position: Int) {
binding.passbookEntry = getItem(position)
holder.itemView.passbookRootLayout.setOnClickListener {
val intent = Intent(activity,OrdersActivity::class.java)
intent.putExtra(Constants.BUNDLE_STRINGS.ORDER_ID,getItem(position)?.referenceId)
activity.startActivity(intent)
}
}
override fun getItemId(position: Int): Long {
return getItem(position)!!.createdAt
}
/*override fun getItemViewType(position: Int): Int {
return position
}*/
}
private val PASSBOOK_DIFF_CALLBACK = object : DiffUtil.ItemCallback<PassbookResponseModel>(){
override fun areItemsTheSame(oldItem: PassbookResponseModel, newItem: PassbookResponseModel): Boolean {
return oldItem.createdAt == newItem.createdAt
}
override fun areContentsTheSame(oldItem: PassbookResponseModel, newItem: PassbookResponseModel): Boolean {
return oldItem == newItem
}
}
The issue is, that you may put recycler view inside NextedScrollview or ScrollView, by removing scrollview it will work fine
I've created a simple project to study Kotlin and Android architecture
https://github.com/AOreshin/shtatus
The screen consists of RecyclerView and three EditTexts.
Corresponding ViewModel is exposing 7 LiveData's:
Three LiveData corresponding to filters
Event to notify the user that no entries are found
Event to notify the user that no entries are present
Status of SwipeRefreshLayout
List of connections to show based on filter input
When user types text in filter ViewModel's LiveData gets notified about the changes and updates the data. I 've read that it's a bad practice to expose MutableLiveData to Activities/Fragments but they have to notify ViewModel about the changes somehow. When no entries are found based on the user's input Toast is shown.
The problem
When the user enters filter values that have no matches, Toast is shown. If the user then rotates the device Toast is shown again and again.
I've read these articles:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
https://proandroiddev.com/livedata-with-single-events-2395dea972a8
But I don't understand how I can apply these to my use case. I think the problem in how I perform the updates
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>().also {
it.value = connections.value
}
private val refreshLiveData = MutableLiveData(RefreshStatus.READY)
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
val nameLiveData = MutableLiveData<String>()
val urlLiveData = MutableLiveData<String>()
val actualStatusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(actualStatusLiveData) { update() }
}
}
fun getRefreshLiveData(): LiveData<RefreshStatus> = refreshLiveData
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
private fun update() {
if (connections.value.isNullOrEmpty()) {
emptyTableEvent.call()
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
noMatchesEvent.call()
}
}
}
update() gets triggered on screen rotation because of new subscription to mediatorConnection and MediatorLiveData.onActive() is called. And it's intented behavior
Android live data - observe always fires after config change
Code for showing toast
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getRefreshLiveData().observe(viewLifecycleOwner, Observer { status ->
when (status) {
ConnectionListViewModel.RefreshStatus.LOADING -> refreshLayout.isRefreshing = true
ConnectionListViewModel.RefreshStatus.READY -> refreshLayout.isRefreshing = false
else -> throwException(status.toString())
}
})
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(nameLiveData.value)
urlEt.setText(urlLiveData.value)
statusCodeEt.setText(actualStatusLiveData.value)
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { nameLiveData.value = it.toString() }
urlEt.addTextChangedListener { urlLiveData.value = it.toString() }
statusCodeEt.addTextChangedListener { actualStatusLiveData.value = it.toString() }
}
}
private fun throwException(status: String) {
throw IllegalStateException(getString(R.string.error_no_such_status) + status)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getRefreshLiveData().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
}
How I should address this issue?
After some head scratching I've decided to go with internal ViewModel statuses, this way logic in Activity/Fragment is kept to a minimum.
So now my ViewModel looks like this:
package com.github.aoreshin.shtatus.viewmodels
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.aoreshin.shtatus.events.SingleLiveEvent
import com.github.aoreshin.shtatus.room.Connection
import io.reactivex.FlowableSubscriber
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subscribers.DisposableSubscriber
import okhttp3.ResponseBody
import retrofit2.Response
import java.util.function.Predicate
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class ConnectionListViewModel #Inject constructor(
private val connectionRepository: ConnectionRepository
) : ViewModel() {
private var tableStatus = TableStatus.OK
private val connections = connectionRepository.allConnections()
private val mediatorConnection = MediatorLiveData<List<Connection>>()
private val stopRefreshingEvent = SingleLiveEvent<Void>()
private val noMatchesEvent = SingleLiveEvent<Void>()
private val emptyTableEvent = SingleLiveEvent<Void>()
private val nameLiveData = MutableLiveData<String>()
private val urlLiveData = MutableLiveData<String>()
private val statusLiveData = MutableLiveData<String>()
init {
with(mediatorConnection) {
addSource(connections) { update() }
addSource(nameLiveData) { update() }
addSource(urlLiveData) { update() }
addSource(statusLiveData) { update() }
}
}
fun getStopRefreshingEvent(): LiveData<Void> = stopRefreshingEvent
fun getNoMatchesEvent(): LiveData<Void> = noMatchesEvent
fun getEmptyTableEvent(): LiveData<Void> = emptyTableEvent
fun getConnections(): LiveData<List<Connection>> = mediatorConnection
fun getName(): String? = nameLiveData.value
fun getUrl(): String? = urlLiveData.value
fun getStatus(): String? = statusLiveData.value
fun setName(name: String) { nameLiveData.value = name }
fun setUrl(url: String) { urlLiveData.value = url }
fun setStatus(status: String) { statusLiveData.value = status }
private fun update() {
if (connections.value != null) {
if (connections.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.EMPTY) {
emptyTableEvent.call()
tableStatus = TableStatus.EMPTY
}
} else {
mediatorConnection.value = connections.value?.filter { connection -> getPredicate().test(connection) }
if (mediatorConnection.value.isNullOrEmpty()) {
if (tableStatus != TableStatus.NO_MATCHES) {
noMatchesEvent.call()
tableStatus = TableStatus.NO_MATCHES
}
} else {
tableStatus = TableStatus.OK
}
}
}
}
fun send() {
if (!connections.value.isNullOrEmpty()) {
val singles = connections.value?.map { connection ->
val id = connection.id
val description = connection.description
val url = connection.url
var message = ""
connectionRepository.sendRequest(url)
.doOnSuccess { message = it.code().toString() }
.doOnError { message = it.message!! }
.doFinally {
val result = Connection(id, description, url, message)
connectionRepository.insert(result)
}
}
Single.mergeDelayError(singles)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { stopRefreshingEvent.call() }
.subscribe(getSubscriber())
} else {
stopRefreshingEvent.call()
}
}
private fun getSubscriber() : FlowableSubscriber<Response<ResponseBody>> {
return object: DisposableSubscriber<Response<ResponseBody>>() {
override fun onComplete() { Log.d(TAG, "All requests sent") }
override fun onNext(t: Response<ResponseBody>?) { Log.d(TAG, "Request is done") }
override fun onError(t: Throwable?) { Log.d(TAG, t!!.message!!) }
}
}
private fun getPredicate(): Predicate<Connection> {
return Predicate { connection ->
connection.description.contains(nameLiveData.value.toString(), ignoreCase = true)
&& connection.url.contains(urlLiveData.value.toString(), ignoreCase = true)
&& connection.actualStatusCode.contains(
statusLiveData.value.toString(),
ignoreCase = true
)
}
}
private enum class TableStatus {
NO_MATCHES,
EMPTY,
OK
}
companion object {
private const val TAG = "ConnectionListViewModel"
}
}
And corresponding Fragment looks like this:
package com.github.aoreshin.shtatus.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.github.aoreshin.shtatus.R
import com.github.aoreshin.shtatus.ShatusApplication
import com.github.aoreshin.shtatus.viewmodels.ConnectionListViewModel
import javax.inject.Inject
class ConnectionListFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var nameEt: EditText
private lateinit var urlEt: EditText
private lateinit var statusCodeEt: EditText
private lateinit var viewModel: ConnectionListViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: ConnectionListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_connection_list, container, false)
val application = (requireActivity().application as ShatusApplication)
application.appComponent.inject(this)
val viewModelProvider = ViewModelProvider(this, viewModelFactory)
viewModel = viewModelProvider.get(ConnectionListViewModel::class.java)
bindViews(view)
setupObservers()
setupListeners()
addFilterValues()
setupRecyclerView()
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState != null) {
refreshLayout.isRefreshing = savedInstanceState.getBoolean(REFRESHING, false)
}
}
private fun setupObservers() {
with(viewModel) {
getConnections().observe(viewLifecycleOwner, Observer { viewAdapter.submitList(it) })
getStopRefreshingEvent().observe(viewLifecycleOwner, Observer { refreshLayout.isRefreshing = false })
getNoMatchesEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_matches) })
getEmptyTableEvent().observe(viewLifecycleOwner, Observer { showToast(R.string.status_no_connections) })
}
}
private fun setupRecyclerView() {
viewAdapter = ConnectionListAdapter(parentFragmentManager, ConnectionItemCallback())
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
private fun addFilterValues() {
with(viewModel) {
nameEt.setText(getName())
urlEt.setText(getUrl())
statusCodeEt.setText(getStatus())
}
}
private fun bindViews(view: View) {
with(view) {
recyclerView = findViewById(R.id.recycler_view)
refreshLayout = findViewById(R.id.refresher)
nameEt = findViewById(R.id.nameEt)
urlEt = findViewById(R.id.urlEt)
statusCodeEt = findViewById(R.id.statusCodeEt)
}
}
private fun setupListeners() {
with(viewModel) {
refreshLayout.setOnRefreshListener { send() }
nameEt.addTextChangedListener { setName(it.toString()) }
urlEt.addTextChangedListener { setUrl(it.toString()) }
statusCodeEt.addTextChangedListener { setStatus(it.toString()) }
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(REFRESHING, refreshLayout.isRefreshing)
}
private fun showToast(resourceId: Int) {
Toast.makeText(context, getString(resourceId), Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
with(viewModel) {
getNoMatchesEvent().removeObservers(viewLifecycleOwner)
getEmptyTableEvent().removeObservers(viewLifecycleOwner)
getStopRefreshingEvent().removeObservers(viewLifecycleOwner)
getConnections().removeObservers(viewLifecycleOwner)
}
}
companion object {
private const val REFRESHING = "isRefreshing"
}
}
Pros
No additional dependencies
Usage of widespread SingleLiveEvent
Pretty straightforward to implement
Cons
Conditional logic is quickly getting out of hand even in this simple case, surely needs refactoring. Not sure if this approach will work in real-life complex scenarios.
If there are cleaner and more concise approaches to solve this problem I will be happy to hear about them!
In your solution; you introduced TableStatus which is acting like flag and not needed.
If you really looking for a good Android Architecture; instead you could just do
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_connections)
and
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
showToast(R.string.status_no_matches)
NOTE:
viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED
is not a patch Google implemented this fix in support library as well.
And remove #Singleton from (why would you need it to be singleton)
#Singleton
class ConnectionListViewModel #Inject constructor(
PS:
From the top of my head looks like; you may also don't need SingleLiveEvent for you case.
(would love to talk more on this if you want I also just have started Kotlin + Clear & Scalable Android Architecture)
I have a spinner in Kotlin and using kotlinx to import it to the code.
This is the code in xml:
<Spinner
android:id="#+id/sp_from_country"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dropDownSelector="#color/colorPrimary"
android:spinnerMode="dropdown"/>
And this is the code in kotlin:
this.sp_from_country.adapter = this.adapter
this.sp_from_country.setSelection(0)
This is where I use the drop down list from a button click event
this.currencyForm = getCurrencyCode(sp_from_country.selectedItem.toString())
The problem is selectedItem is always null.
This is the full source code of my fragment
package training.com.aaptraining.views
import android.app.AlertDialog
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import kotlinx.android.synthetic.main.view_fragment_currency.*
import training.com.aaptraining.R
import training.com.aaptraining.viewmodel.CurrencyViewModel
class CurrencyFragment : Fragment() {
companion object {
fun newInstance() = CurrencyFragment()
}
private val currencies = ArrayList<String>()
private lateinit var currenciesAdapter: ArrayAdapter<String>
private lateinit var currencyForm: String
private lateinit var currencyTo: String
private lateinit var currencyViewModel: CurrencyViewModel
private val spinner: Spinner? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.initViewModel()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.view_fragment_currency, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this.initUI()
this.populateSpinnerAdapter()
}
private fun populateSpinnerAdapter() {
this.currencyViewModel.loadCurrencyList()
?.observe(this, Observer { currencyList ->
currencyList?.forEach {
currencies.add(it.code + " " + it.country)
}
})
this.currenciesAdapter.setDropDownViewResource(R.layout.item_spinner)
this.currenciesAdapter.notifyDataSetChanged()
}
private fun initUI() {
this.initSpinners()
this.initConvertButton()
}
private fun initConvertButton() {
this.btn_convert.setOnClickListener { this.convert() }
}
private fun convert() {
val quantity = edt_value.text.toString()
this.currencyForm = getCurrencyCode(sp_from_country.selectedItem.toString())
this.currencyTo = getCurrencyCode(sp_to_country.selectedItem.toString())
val currencies = "$currencyForm,$currencyTo"
if (quantity.isNotEmpty() && currencyForm != currencyTo) {
this.currencyViewModel.getAvailableExchange(currencies)
?.observe(this, Observer { availableExchange ->
availableExchange?.run {
exchange(quantity.toDouble(), availableExchangeMap)
}
})
}
}
private fun exchange(quantity: Double, availableExchangeMaps: Map<String, Double>) {
val exchangeKeys = availableExchangeMaps.keys.toList()
val exChangesValues = availableExchangeMaps.values.toList()
val fromCurrency = exChangesValues[0]
val toCurrency = exChangesValues[1]
val fromCurrencyKey = this.getCurrencyCodeResult(exchangeKeys[0])
val toCurrencyKey = this.getCurrencyCodeResult(exchangeKeys[1])
val usdExChange = quantity.div(fromCurrency)
val exchangeResult = usdExChange.times(toCurrency)
this.showResult(quantity.toString() + " $fromCurrencyKey = " + exchangeResult.format(4)
+ " $toCurrencyKey")
}
private fun showResult(result: String) {
AlertDialog.Builder(context!!, R.style.AppCompatAlertDialogStyle)
.setMessage(result)
.setTitle("You got the result")
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.setIcon(R.drawable.ic_attach_money_black_24dp)
.create()
.show()
}
private fun getCurrencyCodeResult(currencyCode: String) = currencyCode.substring(3)
private fun getCurrencyCode(currency: String) = currency.substring(0, 3)
private fun initSpinners() {
this.currenciesAdapter = ArrayAdapter(activity, R.layout.item_spinner, currencies)
this.sp_from_country.adapter = this.currenciesAdapter
this.sp_from_country.setSelection(0)
this.sp_to_country.adapter = this.currenciesAdapter
this.sp_to_country.setSelection(0)
this.sp_from_country?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onNothingSelected(p0: AdapterView<*>?) {
Log.d("Currency Framgnet", "On spinner nothing selected")
}
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
Log.d("Currency Framgnet", "On spinner selected")
}
}
}
private fun initViewModel() {
this.currencyViewModel = ViewModelProviders.of(this).get(CurrencyViewModel::class.java)
this.currencyViewModel.let { lifecycle.addObserver(it) }
this.currencyViewModel.initLocalCurrencies()
}
private fun Double.format(digits: Int) = java.lang.String.format("%.${digits}f", this)
}
Consider trying this code snippet to set spinner,
val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, monthList)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner_month.adapter = arrayAdapter
To get the selected item, use this way,
spinner_month.selectedItem.toString()