I'm trying to add offline capabilities to my TMDB app. I've tried doing it with Room but RemoteMediator only loads the first page.
This is how I implemented the RemoteMediator class
#OptIn(ExperimentalPagingApi::class)
class MoviesPopularMediator(
private val service: ApiService,
private val database: PopularMoviesDatabase
) : RemoteMediator<Int, MoviesModel>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MoviesModel>
): MediatorResult {
return try {
val loadKey = when(loadType){
LoadType.REFRESH -> {
1
}
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND ->{
state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
getMoviesPage()
}
}
val response = service.getPopular(
page = state.config.pageSize,
)
val listing = response.body()
val results = listing?.results
if (listing != null) {
database.withTransaction {
if (loadKey != null) {
database.popularMoviesPageDao().savePopularMoviesPage(MoviesPage(page = listing.page, results = listing.results, total_pages = listing.total_pages))
}
if (results != null) {
database.popularMoviesDao().savePopularMovies(results)
}
}
}
MediatorResult.Success(endOfPaginationReached = response.body()?.page == response.body()?.total_pages)
} catch (exception: IOException) {
MediatorResult.Error(exception)
} catch (exception: HttpException) {
MediatorResult.Error(exception)
}
}
private suspend fun getMoviesPage(): MoviesPage? {
return database.popularMoviesPageDao().getPopularMoviesPage().firstOrNull()
}
}
I get the data from this api: https://api.themoviedb.org/3/.
Any ideas on how I should change this RemoteMediator so that it will load all pages?
If you need more details please feel free to ask
Related
My task is to load recyclerView and show pagination which I both implemented.
I also implemented LoadingState for adapter.
ApiCall:
#GET("top-headlines?sources=bbc-news,techcrunch&apiKey=${BuildConfig.API_KEY}")
suspend fun getTopHeadlinesArticles(
#Query("page") page:Int = 1,
#Query("q") query: String,
) : Response<ArticleListResponse>
I wont show paging because it is working so I will jump to repository:
fun getSearchResult(query: String) =
Pager(
config = PagingConfig(
pageSize = 1,
maxSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = { ArticlePaging(newsService, query)}
).flow
ViewModel:
#OptIn(ExperimentalCoroutinesApi::class)
val news = _currentQuery.flatMapLatest { query ->
articleRepository.getSearchResult(query).cachedIn(viewModelScope)
}
Fragment:
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
lifecycleScope.launchWhenCreated {
viewModel.news.collect { articles ->
adapter.submitData(articles)
}
}
And LoadStateViewHolder in LoadStateAdapter:
init {
binding.buttonRetry.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState: LoadState) {
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
buttonRetry.isVisible = loadState !is LoadState.Loading
textViewError.isVisible = loadState !is LoadState.Loading
}
}
I already predefined DEFAULT_QUERY so I only get 1 Article.
Problem is that progressBar loading is always visible and I have no more articles to show.
Edit: when i have at least 10 items to show this works fine
Edit 2nd: Add ArticlePagingSource if that can help
override fun getRefreshKey(state: PagingState<Int, ArticleResponse>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
try {
val currentPageList = params.key ?: 1
response = if(query != ""){
newsService.getTopHeadlinesArticles(currentPageList, query)
} else{
newsService.getTopHeadlinesArticles(currentPageList)
}
val responseList = mutableListOf<ArticleResponse>()
val data = response.body()?.articleResponses ?: emptyList()
responseList.addAll(data)
val prevKey = if (currentPageList == 1) null else currentPageList - 1
return LoadResult.Page(
responseList,
prevKey,
currentPageList.plus(1)
)
I am trying to develop a football app demo. Data comes from an API from the api
It loads data as expected when app started, but when score of match changes, ui is not updating for scores by itself. I am using DiffUtil getChangePayload() to detect changes in score and status fields of Match objects which comes from the response. But it is not triggering when live match data changes. What am i missing?
P.S. I put layout in SwipeRefreshLayout and when i refresh, it gets scores and update the ui. But i want to see the match status and scores updating by itself.
Here is my code:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday: MutableLiveData<List<Matche>> = MutableLiveData()
init {
getMatchesToday()
}
fun getMatchesToday() = viewModelScope.launch {
safeMatchesToday()
}
private suspend fun safeMatchesToday() {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
matchesToday.postValue(it.matches)
}
}
}
}
}
class MatchesTodayFragment : Fragment() {
private var _binding: FragmentMatchesTodayBinding? =null
private val binding get() = _binding!!
private lateinit var mMatchesAdapter: MatchesAdapter
private val viewModel: MatchesViewModel by viewModels {
MatchesViewModelFactory(requireActivity().application, (requireActivity().application as MatchesApplication).repository)
}
#RequiresApi(Build.VERSION_CODES.N)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
viewModel.matchesToday.observe(viewLifecycleOwner) { matches ->
mMatchesAdapter.differ.submitList(matches)
}
binding.srlMatchesToday.setOnRefreshListener {
viewModel.getMatchesToday()
binding.srlMatchesToday.isRefreshing = false
}
}
}
class MatchesAdapter(val fragment: Fragment): RecyclerView.Adapter<MatchesAdapter.ViewHolder>() {
private val differCallback = object: DiffUtil.ItemCallback<Matche>() {
override fun areItemsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.status == newItem.status &&
oldItem.score.fullTime.home == newItem.score.fullTime.home &&
oldItem.score.fullTime.away == newItem.score.fullTime.away &&
oldItem == newItem
}
override fun getChangePayload(oldItem: Matche, newItem: Matche): Any? {
val bundle: Bundle = bundleOf()
if (oldItem.status != newItem.status) {
bundle.apply {
putString(Constants.MATCH_STATUS, newItem.status)
}
}
if (oldItem.score.fullTime.home != newItem.score.fullTime.home) {
bundle.apply {
putInt(Constants.HOME_SCORE, newItem.score.fullTime.home)
}
}
if (oldItem.score.fullTime.away != newItem.score.fullTime.away) {
bundle.apply {
putInt(Constants.AWAY_SCORE, newItem.score.fullTime.away)
}
}
if (bundle.size() == 0) {
return null
}
return bundle
}
}
val differ = AsyncListDiffer(this, differCallback)
#SuppressLint("UseCompatLoadingForDrawables")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val match = differ.currentList[position]
holder.apply {
Glide.with(fragment)
.load(match.homeTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivHomeTeamImage)
Glide.with(fragment)
.load(match.awayTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivAwayTeamImage)
tvHomeTeamName.text = match.homeTeam.name
tvAwayTeamName.text = match.awayTeam.name
when (match.status) {
Constants.TIMED -> {
tvMatchTime.text = Constants.toTimeForTR(match.utcDate)
tvHomeTeamScore.text = "-"
tvAwayTeamScore.text = "-"
}
Constants.PAUSED -> {
tvMatchTime.text = Constants.FIRST_HALF
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
Constants.FINISHED -> {
tvMatchTime.text = Constants.FINISHED
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
else -> {
tvMatchTime.text = Constants.IN_PLAY
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
val item = payloads[0] as Bundle
val status = item.getString(Constants.MATCH_STATUS)
val homeScore = item.getInt(Constants.HOME_SCORE)
val awayScore = item.getInt(Constants.AWAY_SCORE)
holder.apply {
tvMatchTime.text = status
tvHomeTeamScore.text = homeScore.toString()
tvAwayTeamScore.text = awayScore.toString()
Log.e("fuck", status.toString())
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
LiveData only pushes new values if you command it to. Since you want to do it repeatedly, you need to create a loop. This is very easy to do using the liveData coroutine builder.
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
emit(it.matches)
}
}
}
delay(5000) // however many ms you want between fetches
}
}
}
If this is a Retrofit response, I think checking isSuccessful is redundant because body() will be non-null if and only if isSuccessful is true. So it could be simplified a bit from what you have:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
repository.getMatchesToday()?.body()?.matches?.let(::emit)
}
delay(5000) // however many ms you want between fetches
}
}
}
i know this question has been asked several times but everything i can find is only in Java and not very relevant to me...
I'm trying to select a file when i click on a button in my app (images or videos like : /storage/emulated/0/Download/giphy.gif ) and the when the picture or video is selected, i need the path to go inside an edittext.
I have found that code in kotlin for path :
class URIPathHelper {
fun getPath(context: Context, uri: Uri): String? {
val isKitKatorAbove = true
// DocumentProvider
if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).toTypedArray()
val type = split[0]
var contentUri: Uri? = null
if ("image" == type) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else if ("video" == type) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} else if ("audio" == type) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
return getDataColumn(context, uri, null, null)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}
return null
}
fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = uri?.let { context.getContentResolver().query(it, projection, selection, selectionArgs,null) }
if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index)
}
} finally {
if (cursor != null) cursor.close()
}
return null
}
fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
}
And this is what i'm trying :
binding.redloader.setOnClickListener {
val uriPathHelper = URIPathHelper()
val filePath: String? = uriPathHelper.getPath(this, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
val uri: Uri = Uri.parse(filePath)
val intent = Intent(Intent.ACTION_VIEW)
intent.type = "image/*"
intent.putExtra(Intent.ACTION_VIEW, uri)
startActivity(Intent.createChooser(intent, "Open file"))
binding.redgif.setText(filePath)
}
When I click on the button it automatically chooses the first picture in the storage, and then it opens google picture but I cannot do anything except watch the pictures...
I'm sorry I know they are some stupid things in my code, I think I have something to do with intent but all I see on the internet is calling private void and it is not working for me...
I'm a complete beginner and I really hope someone can help me...
Update with what i'm trying, i think i'm close :
binding.redloader.setOnClickListener {
val uriPathHelper = URIPathHelper()
intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent = Intent.createChooser(intent, "Choose a file")
val resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
if (data != null) {
val fileUri: Uri? = data.data
Log.i(LOG_TAG, fileUri.toString())
var fileURIPathHelper: String? = null
try {
fileURIPathHelper = uriPathHelper.getPath(this, EXTERNAL_CONTENT_URI)
} catch (e: Exception) {
Log.e(LOG_TAG, "Error: " + e)
Toast.makeText(this, "Error: " + e, Toast.LENGTH_SHORT).show()
}
this.binding.redgif.setText(fileURIPathHelper)
}
}
}
resultLauncher.launch(intent)
}
I solved it ! :)
val resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
if (data != null) {
val fileUri: Uri? = data.data
Log.i(PackageManagerCompat.LOG_TAG, fileUri.toString())
val contentUri: String?
try {
contentUri = uriPathHelper.getPath(this, fileUri!!)
this.binding.redgif.setText(contentUri)
} catch (e: Exception) {
Log.e(PackageManagerCompat.LOG_TAG, "Error: " + e)
Toast.makeText(this, "Error: " + e, Toast.LENGTH_SHORT).show()
}
}
}
}
binding.redloader.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
startActivity(Intent.createChooser(intent, "Open file"))
resultLauncher.launch(intent)
}
I'm writing an app using Kotlin.
I'am using ActivityResultLauncher for get permission. I can use ActivityResultLauncher on Activity clearly but when I try to get permission on fragment with ActivityResultLauncher, the app goint to background without getting any error. I will put my code below. Please help me :)
private var selectedPictureUri: Uri? = null
private lateinit var permissionLauncherForGallery: ActivityResultLauncher<String>
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun populateUI(savedInstanceState: Bundle?) {
setClickListener()
registerLauncher()
}
private fun selectMedia() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
//request permission
if (ActivityCompat.shouldShowRequestPermissionRationale(
this#WriteUsActivity,
Manifest.permission.READ_EXTERNAL_STORAGE
)
) {
Snackbar.make(
mBinding!!.root,
"Permission needed for Gallery",
Snackbar.LENGTH_INDEFINITE
).setAction("Give Permission") {
permissionLauncherForGallery.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}.show()
} else {
permissionLauncherForGallery.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
val intentToGallery =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
activityResultLauncher.launch(intentToGallery)
}
}
private fun registerLauncher() {
activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val intentFromResult = result.data
if (intentFromResult != null) {
selectedPictureUri = intentFromResult.data
if (selectedPictureUri != null) {
mBinding?.ivPhoto?.visibility= View.VISIBLE
mBinding?.ivPhoto?.setImageURI(selectedPictureUri)
} else {
val bundle = result.data!!.extras
val bitmap = bundle?.get("data")
mBinding?.ivPhoto?.setImageBitmap(bitmap as Bitmap)
}
}
}
}
permissionLauncherForGallery =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
if (result) {
//permission granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
val intentToGallery =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
activityResultLauncher.launch(intentToGallery)
}
} else {
//permission denied
Toast.makeText(this, "Permisson needed!", Toast.LENGTH_LONG).show()
}
}
}
private fun setClickListener() {
mBinding?.apply {
btnPhoto.setOnClickListener {
selectMedia()
}
btnPhoto2.setOnClickListener {
this#WriteUsActivity.toast("Pick Second Photo")
}
btnSend.setOnClickListener {
this#WriteUsActivity.toast("Save")
}
}
}
I have the following problem that I am already working on for over 20 hours: I want to use an Event Channel to get a data stream from the Spotify SDK. On the native side, I can automatically display the status of a current song by subscribing to my PlayerState. My goal is to be able to access this data stream with my Flutter app. On the native side I can output the data flow without problems. But I also want to be able to access this data in my Flutter App. The problem is that I do not get the data from Kotlin to Dart. I can not execute the command mEventSink?.success(position) because the mEventSink is zero.
It would be really great if someone could help me with this problem.
//...
class Spotifysdk04Plugin(private var registrar: Registrar): MethodCallHandler, EventChannel.StreamHandler {
//...
private var mEventSink: EventChannel.EventSink? = null
companion object {
#JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "spotifysdk")
channel.setMethodCallHandler(Spotifysdk04Plugin(registrar))
val eventChannel = EventChannel(registrar.messenger(), "timerStream")
eventChannel.setStreamHandler(Spotifysdk04Plugin(registrar))
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "loginAppRemote") {
//...
} else if(call.method == "initEventStream") {
try {
spotifyAppRemote!!.playerApi.subscribeToPlayerState()
.setEventCallback { playerState: PlayerState? ->
Log.d("test", "test24")
var position = playerState!!.playbackPosition.toDouble()
Log.d("playbackPosition1", position.toString())
if(mEventSink != null) {
Log.d("test", "test25")
mEventSink?.success(position)
} else {
Log.d("test", "mEventSink == null")
}
}
} catch (err:Throwable) {
Log.v("initEventStreamError",err.message.toString())
result.success(false)
}
} else {
result.notImplemented()
}
}
override fun onCancel(arguments: Any?) {
mEventSink = null
}
override fun onListen(arguments: Any?, eventSink: EventChannel.EventSink) {
mEventSink = eventSink
}
}
I found a solution:
override fun onListen(p0: Any?, p1: EventChannel.EventSink?) {
mEventSink = p1
Log.d("test", "test1")
if(spotifyAppRemote == null) {
Log.d("test", "test2")
}
val connectionParams = ConnectionParams.Builder(clientId)
.setRedirectUri(redirectUri)
.showAuthView(true)
.build()
SpotifyAppRemote.connect(registrar.context(), connectionParams, object : Connector.ConnectionListener {
override fun onConnected(appRemote: SpotifyAppRemote) {
spotifyAppRemote = appRemote
if(spotifyAppRemote != null) {
Log.d("test", "test3")
spotifyAppRemote!!.playerApi.subscribeToPlayerState()
.setEventCallback { playerState: PlayerState? ->
Log.d("test", "test24")
var position = playerState!!.playbackPosition.toDouble()
Log.d("playbackPosition1", position.toString())
if(mEventSink != null) {
Log.d("test", "test25")
mEventSink?.success(position)
} else {
Log.d("test", "mEventSink == null")
}
}
}
Log.d("Spotify App Remote Login", "Connected!")
}
override fun onFailure(throwable: Throwable) {
Log.e("Spotify App Remote Login", "Error!", throwable)
}
})
}