Related
Fragment Recyclerview code :
private fun initRecyclerView() {
bestSellersAdapter = BestSellerProductsAdapter(frag)
homeBinding.popularproductsRView.apply {
bestSellersAdapter.second(this#HomeFragment)
adapter = bestSellersAdapter
}
}
private fun getSection() {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
roomProductVM.productFlow.collectLatest {
if (it.isEmpty()) {
categoryViewModel.getAllCategories()
} else {
bestSellersAdapter.setMutableArraylist(it)
bestSellersAdapter.notifyDataSetChanged()
}
}
}
}
based on condition i call this function(getSection) in onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
getSection()
addcartviewModel.cartTableSuccessorNot.observe(viewLifecycleOwner) {
if(it.toInt() == variant_id) {
Toast.makeText(context,"Product Updated Successfully",Toast.LENGTH_SHORT).show() homeBinding.popularproductsRView.adapter?.notifyItemChanged(productPosition)
} else {
LoadingUtils.hideDialog()
Toast.makeText(context,"Failure in Cart Table", Toast.LENGTH_SHORT).show()
}
}
}
interface function in fragment : ( here i am taking product position from adapter )
override fun updatepro(position: Int, type: String, id: Int, cart_count: Int, is_notify: Boolean, product_id: Int, name: String, measurement: String,
variant_image: String, price: String, qty: String, stock: String, image: String, moq: String, unit: String) {
productPosition = position
variant_id = id
variant_qty = qty.toInt()
addcartviewModel.updateVariantTableinRoom(id,cart_count.toString(),is_notify,product_id)
addcartviewModel.insertVariantListintoCartTable(id,measurement,price,stock,image,moq,name,variant_image,unit,qty)
}
Adapter class :
class BestSellerProductsAdapter(var context: Context) : RecyclerView.Adapter<BestSellerProductsAdapter.BestSellerViewHolder>() {
var mutableList = mutableListOf<ProductWithVariants>()
fun setMutableArraylist(datas: MutableList<ProductWithVariants>) {
mutableList.clear()
mutableList.addAll(datas)
}
override fun onBindViewHolder(holder: BestSellerViewHolder, position: Int) {
var cart_count = mutableList[position].variantsList[0].cart_count
if(stock?.toInt()==0) {
holder.notify.visibility = View.VISIBLE
holder.addtocart.visibility = View.GONE
holder.pll_cart.visibility = View.GONE
} else if(cart_count == 0) {
holder.addtocart.visibility = View.VISIBLE
holder.pll_cart.visibility = View.GONE
holder.notify.visibility = View.GONE
} else {
holder.notify.visibility = View.GONE
holder.addtocart.visibility = View.GONE
holder.pll_cart.visibility = View.VISIBLE
holder.tv_cart.text = cart_count.toString()
}
holder.addtocart.setOnClickListener {
cart_count = ((cart_count ?: 0) + 1)
selectpro?.updatepro(position,"AddCart",mutableList[position].variantsList[0].id,mutableList[position].variantsList[0].cart_count!!.toInt(),false,mutableList[position].products.id,
mutableList[position].products.name.toString(),
mutableList[position].variantsList[0].measurement.toString() + mutableList[position].variantsList[0].measurement_unit_name.toString(),
mutableList[position].variantsList[0].image.toString(),
mutableList[position].variantsList[0].discounted_price.toString(),
cart_count.toString(),
stock.toString(),
mutableList[position].variantsList[0].image.toString(),
moq.toString(),
mutableList[position].variantsList[0].measurement_unit_name.toString()
)
}
var selectpro: selectproduct?=null
fun second(selectproduct: selectproduct) {
selectpro = selectproduct
}
interface selectproduct {
fun updatepro(position:Int,type:String,id:Int, cart_count:Int,is_notify:Boolean,product_id:Int,
name:String,measurement:String,variant_image:String,price:String,qty:String,
stock:String,image:String,moq:String,unit:String)
fun selectProduct(product: ProductWithVariants,position:Int)
}
Now i want to update the particular item of recycler view after Toast ( Product Updated Successfully ) in fragment .. how to do that
1 After response first find out the item position you want to update
in adapter
2 Also you have to update the list in adapter with new list of data
you get from the response.
then use below code to update particular item in adapter using
position.
adapter.notifyItemChanged(position)
I have this app which displays a list of "coins" to the users . This list was parsed from an JSON API and I used Jetpack Compose for the UI. I implemented
Here is the code of the Jetpack composable list of "coins"
#Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel(),
) {
val state = viewModel.state.value
Surface {
Box(modifier = Modifier.fillMaxSize()) {
Column {
androidx.compose.foundation.Image(painter = painterResource(id = R.drawable.ic_baseline_currency_bitcoin_24),
contentDescription = "BTC",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
.size(50.dp, 50.dp)
)
SearchBar(
hint = "Search..",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
){
viewModel.searchCoinsList(it) **//here I'm calling my search function from the view model, inside my search bar**
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) { coin ->
Spacer(modifier = Modifier.height(5.dp))
CoinListItem(
coin = coin,
onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${coin.id}")
}
)
Divider()
}
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
}
**//and this Is my composable search bar**
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = "",
onSearch: (String) -> Unit = {}
) {
var text by remember {
mutableStateOf("")
}
var isHint by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier){
BasicTextField(
value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHint = it.isFocused != true
}
)
if(isHint){
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
and this is my view model, this is where I'm implementing the search function, this is where I'm lost, variables that I'm searching for are name, rank, and symbol from the Coin domain list
#HiltViewModel //injecting the use case
class CoinListViewModel #Inject constructor (
private val getCoinsUseCase: GetCoinsUseCase,
) : ViewModel() {
//vmstate Live Template, only the view model touches it
private val _state =
mutableStateOf(CoinListState())
val state: State<CoinListState> = _state
**//for search purposes , this is where I'm lost**
private var coinsList = mutableStateOf<List<Coin>>(listOf())
private var cachedCoinsList = listOf<Coin>()
private var isSearchStarting = true
private var isSearching = mutableStateOf(false)
init {
getCoins()
}
**//for search purposes , this is where I'm lost**
fun searchCoinsList(query: String){
val listToSearch = if(isSearchStarting){
coinsList.value
} else {
cachedCoinsList
}
viewModelScope.launch(Dispatchers.Default) {
if(query.isEmpty()){
coinsList.value = cachedCoinsList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
//val iterate: Int = coins.size
it.name.contains(query.trim(), ignoreCase = true) ||
(it.rank.toString() == query.trim()) ||
it.symbol.contains(query.trim(), ignoreCase = true)
}
if(isSearchStarting){
cachedCoinsList = coinsList.value
isSearchStarting = false
}
coinsList.value = results
isSearching.value = true
}
}
//function that calls our GetCoinsUseCase and puts the data inside the state object
//to display that in the UI
private fun getCoins() {
//overwrote the invoke function earlier for the use case which allows us to call the use case as a function
getCoinsUseCase().onEach { result ->
when (result) {
is Resource.SUCCESS -> {
_state.value =
CoinListState(coins = result.data ?: arrayListOf())
}
is Resource.ERROR -> {
_state.value =
CoinListState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.LOADING -> {
_state.value = CoinListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
CoinsListState data class used in view model
data class CoinListState(
val isLoading: Boolean = false,
val coins: ArrayList<Coin> = arrayListOf(),
val error: String = ""
)
this is my "GetCoinsUseCase" to get the coins
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
// overwriting the operator fun invoke allows us to call the use case
//GetCoinsUseCase as if it was a function, and we return a flow because
// we want to emit states LOADING -> for progress bar, SUCCESS -> attach list of coins,
// and ERROR
operator fun invoke(): kotlinx.coroutines.flow.Flow<Resource<ArrayList<Coin>>> = flow {
try {
emit(Resource.LOADING<ArrayList<Coin>>())
//we mapped it to toCoin because we returning a list of coin, not coinDTO
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.SUCCESS<ArrayList<Coin>>(coins as ArrayList<Coin>))
}catch (e: HttpException){
emit(Resource.ERROR<ArrayList<Coin>>(e.localizedMessage ?: "An unexpected error occurred"))
}catch (e: IOException){
emit(Resource.ERROR<ArrayList<Coin>>("Couldn't reach server. Check connection"))
}
}
}
just the coin repository that is implemented in another place
interface CoinRepository {
//repository definitions
suspend fun getCoins() : ArrayList<CoinDTO>
suspend fun getCoinById(coinId: String) : CoinDetailDTO
}
This is my domain - Domain - only contains the data needed
data class Coin(
var id: String,
var isActive: Boolean,
var name: String,
var rank: Int,
var symbol: String
)
and this is how I'm mapping it
data class CoinDTO(
val id: String,
#SerializedName("is_active")
val isActive: Boolean,
#SerializedName("is_new")
val isNew: Boolean,
val name: String,
val rank: Int,
val symbol: String,
val type: String
)
fun CoinDTO.toCoin(): Coin {
return Coin(
id = id,
isActive = isActive,
name = name,
rank = rank,
symbol = symbol,
// logo = CoinDetailLogo(logo = String()).logo
)
}
Coin list item if needed for reference, this is what is displayed to the user in the list
#Composable
fun CoinListItem (
coin: Coin,
onItemClick: (Coin) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClick(coin) }
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "${coin.rank}. ${coin.name} (${coin.symbol})",
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis
)
Text(
text = if(coin.isActive) "active" else "inactive",
color = if(coin.isActive) Color.Green else Color.Red,
fontStyle = FontStyle.Italic,
textAlign = TextAlign.End,
style = MaterialTheme.typography.body2,
modifier = Modifier.align(CenterVertically)
)
}
}
as well as the "Resource" generic for states
//UIStates
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class SUCCESS<T>(data: T) : Resource<T>(data)
class ERROR<T>(message: String, data: T? = null) : Resource<T>(data, message)
class LOADING<T>(data: T? = null) : Resource<T>(data)
}
again, given this info, how can I get the function searchCoinList in the view model to correctly view the searched data (name, rank, or symbol) when it is called in the CoinListScreen inside the Search Bar. Thank you so much
It seems like you want to implement a basic instant search functionality. It's pretty easy to achieve using Kotlin's StateFlow and its operators. Consider the following implementation with description:
// CoinListViewModel
private val queryFlow = MutableStateFlow("")
private val coinsList = mutableStateOf<List<Coin>>(listOf())
init {
queryFlow
.debounce(300) // filters out values that are followed by the newer values within the given timeout. The latest value is always emitted.
.filterNot { query -> userInput.isEmpty() } // filter the unwanted string like an empty string in this case to avoid the unnecessary network call.
.distinctUntilChanged() // to avoid duplicate network calls
.flowOn(Dispatchers.IO) // Changes the context where this flow is executed to Dispatchers.IO
.flatMapLatest { query -> // to avoid the network call results which are not needed more for displaying to the user
getCoinsUseCase(query).catch { emitAll(flowOf(emptyList())}
}
.onEach { coins: List<Coin> -> // go through each list of Coins
coinsList.value = coins
}
.launchIn(viewModelScope)
}
fun searchCoinsList(query: String) {
queryFlow.value = query
}
I have an Android app with a map in MapBox where I'm asking for the user's location. It pops up the permission request correctly, but on clicking "Allow" the camera shows a region of Africa, instead of the proper coordinates. It's taking the correct coordinates, it's just the camera that doesn't focus.
This is my LocationPermissionHelper
class LocationPermissionHelper(val activity: WeakReference<Activity>) {
private lateinit var permissionsManager: PermissionsManager
fun checkPermissions(onMapReady: () -> Unit) {
if (PermissionsManager.areLocationPermissionsGranted(activity.get())) {
Log.e("permission", "I'm already granted")
onMapReady()
} else {
Log.e("permission", "They are not granted")
permissionsManager = PermissionsManager(object : PermissionsListener {
override fun onExplanationNeeded(permissionsToExplain: List<String>) {
Log.e("permission", "Inside onExplanation needed")
Toast.makeText(
activity.get(), "You need to accept location permissions.",
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionResult(granted: Boolean) {
Log.e("permission", "Inside onPermissionResult needed before if statement")
if (granted) {
Log.e("permission", "I just got granted")
onMapReady()
} else {
Log.e("permission", "Inside onPermissionResult needed NOT GRANTED")
activity.get()?.finish()
}
}
})
permissionsManager.requestLocationPermissions(activity.get())
}
}
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
And fragment:
class AddMapFragment : Fragment(R.layout.fragment_add_map) {
private lateinit var locationPermissionHelper: LocationPermissionHelper
private lateinit var binding: FragmentAddMapBinding
private lateinit var mapView: MapView
private lateinit var fusedLocationClient: FusedLocationProviderClient
private var userLat: Double = 41.409428
private var userLong: Double = 2.111255
private var boarLat: Double = 41.409428
private var boarLong: Double = 2.111255
#SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAddMapBinding.bind(view)
fusedLocationClient =
LocationServices.getFusedLocationProviderClient(activity as MainActivity)
mapView = binding.mapAdd
// Check location permissions
locationPermissionHelper = LocationPermissionHelper(WeakReference(activity))
locationPermissionHelper.checkPermissions {
// configure map
onMapReady()
// store user location coordinates
fusedLocationClient.lastLocation
.addOnSuccessListener { location: Location? ->
userLat = location?.latitude ?: 41.409428
userLong = location?.longitude ?: 2.111255
}
}
// Button listener
with(binding) {
fabAddMap.setOnClickListener {
findNavController().navigate(R.id.action_addMapFragment_to_addFormFragment,
Bundle().apply {
putDouble("lat", boarLat)
putDouble("long", boarLong)
}
)
}
}
}
// get map and set camera
fun onMapReady() {
mapView.getMapboxMap().setCamera(
CameraOptions.Builder()
.center(Point.fromLngLat(userLong, userLat))
.pitch(45.0)
.zoom(14.0)
.bearing(-17.6)
.build()
)
mapView.getMapboxMap().loadStyleUri(
Style.MAPBOX_STREETS
) {
initLocationComponent()
setupGesturesListener()
addAnnotationToMap()
}
}
private fun initLocationComponent() {
val locationComponentPlugin = mapView.location
locationComponentPlugin.updateSettings {
this.enabled = true
this.pulsingEnabled = true
this.locationPuck = LocationPuck2D(
bearingImage = AppCompatResources.getDrawable(
requireContext(),
com.mapbox.maps.plugin.locationcomponent.R.drawable.mapbox_user_icon,
),
shadowImage = AppCompatResources.getDrawable(
requireContext(),
com.mapbox.maps.plugin.locationcomponent.R.drawable.mapbox_user_icon_shadow,
),
scaleExpression = interpolate {
linear()
zoom()
stop {
literal(0.0)
literal(0.6)
}
stop {
literal(20.0)
literal(1.0)
}
}.toJson()
)
}
// Pass the user's location to camera
locationComponentPlugin.addOnIndicatorPositionChangedListener(
onIndicatorPositionChangedListener
)
}
#SuppressLint("MissingPermission")
private fun addAnnotationToMap() {
// Create an instance of the Annotation API and get the PointAnnotationManager.
bitmapFromDrawableRes(
requireContext(),
R.drawable.boar
)?.let {
val annotationApi = mapView.annotations
val pointAnnotationManager = annotationApi.createPointAnnotationManager()
// Set options for the resulting symbol layer.
val pointAnnotationOptions: PointAnnotationOptions = PointAnnotationOptions()
// Define a geographic coordinate.
.withPoint(Point.fromLngLat(userLong, userLat))
// Point.fromLngLat(2.111255, 41.409428)
// Specify the bitmap you assigned to the point annotation
.withIconImage(it)
// make marker draggable
.withDraggable(true)
// Add the resulting pointAnnotation to the map.
pointAnnotationManager.addDragListener(dragListener)
pointAnnotationManager.create(pointAnnotationOptions)
}
}
private val dragListener = object : OnPointAnnotationDragListener {
override fun onAnnotationDrag(annotation: Annotation<*>) {}
override fun onAnnotationDragStarted(annotation: Annotation<*>) {}
override fun onAnnotationDragFinished(annotation: Annotation<*>) {
boarLat = (annotation as PointAnnotation).point.latitude()
boarLong = annotation.point.longitude()
/*
Toast.makeText(
requireContext(),
"Lat: $latitude, Long: $longitude, Time: $time",
Toast.LENGTH_SHORT
).show()
*/
}
}
private fun setupGesturesListener() {
mapView.gestures.addOnMoveListener(onMoveListener)
mapView.gestures.addOnMapClickListener { point ->
mapView.location
.isLocatedAt(point) { isPuckLocatedAtPoint ->
if (isPuckLocatedAtPoint) {
Toast.makeText(
requireContext(),
"Clicked on location puck ${mapView.location}",
Toast.LENGTH_SHORT
).show()
}
}
true
}
mapView.gestures.addOnMapLongClickListener { point ->
mapView.location
.isLocatedAt(point) { isPuckLocatedAtPoint ->
if (isPuckLocatedAtPoint) {
Toast.makeText(
requireContext(),
"Long-clicked on location puck ${mapView.location}",
Toast.LENGTH_SHORT
).show()
}
}
true
}
}
// CAMERA TRACKING
private val onIndicatorBearingChangedListener = OnIndicatorBearingChangedListener {
mapView.getMapboxMap().setCamera(CameraOptions.Builder().bearing(it).build())
}
private val onIndicatorPositionChangedListener = OnIndicatorPositionChangedListener {
// Jump to the current indicator position
mapView.getMapboxMap().setCamera(CameraOptions.Builder().center(it).build())
// Set the gestures plugin's focal point to the current indicator location.
mapView.gestures.focalPoint = mapView.getMapboxMap().pixelForCoordinate(it)
}
private val onMoveListener = object : OnMoveListener {
override fun onMoveBegin(detector: MoveGestureDetector) {
onCameraTrackingDismissed()
}
override fun onMove(detector: MoveGestureDetector): Boolean {
return false
}
override fun onMoveEnd(detector: MoveGestureDetector) {}
}
private fun onCameraTrackingDismissed() {
Toast.makeText(requireContext(), "onCameraTrackingDismissed", Toast.LENGTH_SHORT).show()
mapView.location
.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener)
mapView.gestures.removeOnMoveListener(onMoveListener)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
locationPermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onDestroyView() {
super.onDestroyView()
mapView.location
.removeOnIndicatorBearingChangedListener(onIndicatorBearingChangedListener)
mapView.location
.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener)
mapView.gestures.removeOnMoveListener(onMoveListener)
}
}
IMAGES:
I do not understand why this code does not work to update a list when navigating "back" from a DetailView(). As far as I can tell, I'm calling a new fetchRequest each time I want to update the list and it seems that request should always return object with current properties. But as others have said they are "stale", reflecting whatever was the property BEFORE the update was committed in the DetailView. And tapping a Navigation link from a "Stale" row, opens a DetailView with the current values of the properties, so I know they have been sacved to the context (haven't they?).
First I have a "dataservice" like this:
import CoreData
import SwiftUI
protocol CategoryDataServiceProtocol {
func getCategories() -> [Category]
func getCategoryById(id: NSManagedObjectID) -> Category?
func addCategory(name: String, color: String)
func updateCategory(_ category: Category)
func deleteCategory(_ category: Category)
}
class CategoryDataService: CategoryDataServiceProtocol {
var viewContext: NSManagedObjectContext = PersistenceController.shared.viewContext
///Shouldn't this next function always return an updated version of my list of categories?
func getCategories() -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
let sort: NSSortDescriptor = NSSortDescriptor(keyPath: \Category.name_, ascending: true)
request.sortDescriptors = [sort]
///This line appears to do nothing if I insert it:
request.shouldRefreshRefetchedObjects = true
do {
///A print statement here does run, so it's getting this far...
print("Inside get categories func")
return try viewContext.fetch(request)
} catch {
return []
}
}
func getCategoryById(id: NSManagedObjectID) -> Category? {
do {
return try viewContext.existingObject(with: id) as? Category
} catch {
return nil
}
}
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
func updateCategory(_ category: Category) {
saveContext()
}
func deleteCategory(_ category: Category) {
viewContext.delete(category)
saveContext()
}
func saveContext() {
PersistenceController.shared.save()
}
}
class MockCategoryDataService: CategoryDataService {
override init() {
super .init()
self.viewContext = PersistenceController.preview.viewContext
print("MOCK INIT")
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
}
}
And I have a viewModel like this:
import SwiftUI
extension CategoriesList {
class ViewModel: ObservableObject {
let dataService: CategoryDataServiceProtocol
#Published var categories: [Category] = []
init(dataService: CategoryDataServiceProtocol = CategoryDataService()) {
self.dataService = dataService
}
func getCategories() {
self.categories = dataService.getCategories()
}
func deleteCategories(at offsets: IndexSet) {
offsets.forEach { index in
let category = categories[index]
dataService.deleteCategory(category)
}
}
}
}
Then my view:
import SwiftUI
struct CategoriesList: View {
#StateObject private var viewModel: CategoriesList.ViewModel
init(viewModel: CategoriesList.ViewModel = .init()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
#State private var isShowingSheet = false
var body: some View {
NavigationView {
List {
ForEach(viewModel.categories) { category in
NavigationLink(
destination: CategoryDetail(category: category)) {
CategoryRow(category: category)
.padding(0)
}
}
.onDelete(perform: { index in
viewModel.deleteCategories(at: index)
viewModel.getCategories()
})
}
.listStyle(PlainListStyle())
.onAppear(perform: {
viewModel.getCategories()
})
.navigationBarTitle(Text("Categories"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: { EditButton() })
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: {
isShowingSheet = true
viewModel.getCategories()
},
label: { Image(systemName: "plus.circle").font(.system(size: 20)) }
)
}
}
.sheet(isPresented: $isShowingSheet, onDismiss: {
viewModel.getCategories()
}, content: {
CategoryForm()
})
}
}
}
struct CategoriesList_Previews: PreviewProvider {
static var previews: some View {
let viewModel: CategoriesList.ViewModel = .init(dataService: MockCategoryDataService())
return CategoriesList(viewModel: viewModel)
}
}
So, when I navigate to the DetailView and change the name of the category, all is fine there. But then tapping the back button or swiping to return to the view - and the view still shows the old name.
I understand that the #Published array of [Category] is probably not looking at changes to objects inside the array, only if an object is removed or added, I guess.
But why is my list not updating anyways, since I am calling viewModel.getCategories() and that is triggering the fetch request in the dataservice getCategories function?
And if Combine is the answer, then how? Or what else am I missing? Does request.shouldRefreshRefetchedObjects = true offer anything? Or is it a bug as I read here: https://mjtsai.com/blog/2019/10/17/core-data-derived-attributes/
I have a remote server from where I want to fetch 20 items(Job) per api call and show them in RecyclerView using paging library.
For that, I want to show a loading indicator at the beginning of the first api call when list of items is being fetched from the server. Everything is okay if data is fetched successfully. That means the loading indicator got invisible if data loaded successfully. The code is given bellow.
JobService.KT
#GET(Constants.API_JOB_LIST)
fun getJobPost(
#Query("page") pageNumber: Int
): Observable<Response<JobResponse>>
JobResponse.kt
data class JobResponse(
#SerializedName("status") val status: Int? = null,
#SerializedName("message") val message: Any? = null,
#SerializedName("data") val jobData: JobData? = null
)
JobData.kt
data class JobData(
#SerializedName("jobs") val jobs: List<Job?>? = null,
#SerializedName("total") val totalJob: Int? = null,
#SerializedName("page") val currentPage: Int? = null,
#SerializedName("showing") val currentlyShowing: Int? = null,
#SerializedName("has_more") val hasMore: Boolean? = null
)
NetworkState.kt
sealed class NetworkState {
data class Progress(val isLoading: Boolean) : NetworkState()
data class Failure(val errorMessage: String?) : NetworkState()
companion object {
fun loading(isLoading: Boolean): NetworkState = Progress(isLoading)
fun failure(errorMessage: String?): NetworkState = Failure(errorMessage)
}
}
Event.kt
open class Event<out T>(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled() = if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
fun peekContent() = content
}
JobDataSource.kt
class JobDataSource(
private val jobService: JobService,
private val compositeDisposable: CompositeDisposable
) : PageKeyedDataSource<Int, Job>() {
val paginationState: MutableLiveData<Event<NetworkState>> = MutableLiveData()
val initialLoadingState: MutableLiveData<Event<NetworkState>> = MutableLiveData()
val totalJob: MutableLiveData<Event<Int>> = MutableLiveData()
companion object {
private const val FIRST_PAGE = 1
}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Job>) {
compositeDisposable += jobService.getJobPost(FIRST_PAGE)
.performOnBackgroundOutputOnMain()
.doOnSubscribe { initialLoadingState.postValue(Event(loading(true))) }
.doOnTerminate { initialLoadingState.postValue(Event(loading(false))) }
.subscribe({
if (it.isSuccessful) {
val jobData = it.body()?.jobData
totalJob.postValue(Event(jobData?.totalJob!!))
jobData.jobs?.let { jobs -> callback.onResult(jobs, null, FIRST_PAGE+1) }
} else {
val error = Gson().fromJson(it.errorBody()?.charStream(), ApiError::class.java)
when (it.code()) {
CUSTOM_STATUS_CODE -> initialLoadingState.postValue(Event(failure(error.message!!)))
else -> initialLoadingState.postValue(Event(failure("Something went wrong")))
}
}
}, {
if (it is IOException) {
initialLoadingState.postValue(Event(failure("Check Internet Connectivity")))
} else {
initialLoadingState.postValue(Event(failure("Json Parsing error")))
}
})
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Job>) {
compositeDisposable += jobService.getJobPost(params.key)
.performOnBackgroundOutputOnMain()
.doOnSubscribe { if (params.key != 2) paginationState.postValue(Event(loading(true))) }
.doOnTerminate { paginationState.postValue(Event(loading(false))) }
.subscribe({
if (it.isSuccessful) {
val jobData = it.body()?.jobData
totalJob.postValue(Event(jobData?.totalJob!!))
jobData.jobs?.let { jobs -> callback.onResult(jobs, if (jobData.hasMore!!) params.key+1 else null) }
} else {
val error = Gson().fromJson(it.errorBody()?.charStream(), ApiError::class.java)
when (it.code()) {
CUSTOM_STATUS_CODE -> initialLoadingState.postValue(Event(failure(error.message!!)))
else -> initialLoadingState.postValue(Event(failure("Something went wrong")))
}
}
}, {
if (it is IOException) {
paginationState.postValue(Event(failure("Check Internet Connectivity")))
} else {
paginationState.postValue(Event(failure("Json Parsing error")))
}
})
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Job>) {}
}
JobDataSourceFactory.kt
class JobDataSourceFactory(
private val jobService: JobService,
private val compositeDisposable: CompositeDisposable
): DataSource.Factory<Int, Job>() {
val jobDataSourceLiveData = MutableLiveData<JobDataSource>()
override fun create(): DataSource<Int, Job> {
val jobDataSource = JobDataSource(jobService, compositeDisposable)
jobDataSourceLiveData.postValue(jobDataSource)
return jobDataSource
}
}
JobBoardViewModel.kt
class JobBoardViewModel(
private val jobService: JobService
) : BaseViewModel() {
companion object {
private const val PAGE_SIZE = 20
private const val PREFETCH_DISTANCE = 20
}
private val jobDataSourceFactory: JobDataSourceFactory = JobDataSourceFactory(jobService, compositeDisposable)
var jobList: LiveData<PagedList<Job>>
init {
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.setEnablePlaceholders(false)
.build()
jobList = LivePagedListBuilder(jobDataSourceFactory, config).build()
}
fun getPaginationState(): LiveData<Event<NetworkState>> = Transformations.switchMap<JobDataSource, Event<NetworkState>>(
jobDataSourceFactory.jobDataSourceLiveData,
JobDataSource::paginationState
)
fun getInitialLoadingState(): LiveData<Event<NetworkState>> = Transformations.switchMap<JobDataSource, Event<NetworkState>>(
jobDataSourceFactory.jobDataSourceLiveData,
JobDataSource::initialLoadingState
)
fun getTotalJob(): LiveData<Event<Int>> = Transformations.switchMap<JobDataSource, Event<Int>>(
jobDataSourceFactory.jobDataSourceLiveData,
JobDataSource::totalJob
)
}
JobBoardFragment.kt
class JobBoardFragment : BaseFragment() {
private val viewModel: JobBoardViewModel by lazy {
getViewModel { JobBoardViewModel(ApiFactory.jobListApi) }
}
private val jobAdapter by lazy {
JobAdapter {
val bundle = Bundle()
bundle.putInt(CLICKED_JOB_ID, it.jobId!!)
navigateTo(R.id.jobBoard_to_jobView, R.id.home_navigation_fragment, bundle)
}
}
override fun getLayoutResId() = R.layout.fragment_job_board
override fun initWidget() {
job_list_recycler_view.adapter = jobAdapter
back_to_main_image_view.setOnClickListener { onBackPressed() }
}
override fun observeLiveData() {
with(viewModel) {
jobList.observe(this#JobBoardFragment, Observer {
jobAdapter.submitList(it)
})
getInitialLoadingState().observe(this#JobBoardFragment, Observer {
it.getContentIfNotHandled()?.let { state ->
when (state) {
is Progress -> {
if (state == loading(true)) {
network_loading_indicator.visible()
} else {
network_loading_indicator.visibilityGone()
}
}
is Failure -> context?.showToast(state.errorMessage.toString())
}
}
})
getPaginationState().observe(this#JobBoardFragment, Observer {
it.getContentIfNotHandled()?.let { state ->
when (state) {
is Progress -> {
if (state == loading(true)) {
pagination_loading_indicator.visible()
} else {
pagination_loading_indicator.visibilityGone()
}
}
is Failure -> context?.showToast(state.errorMessage.toString())
}
}
})
getTotalJob().observe(this#JobBoardFragment, Observer {
it.getContentIfNotHandled()?.let { state ->
job_board_text_view.visible()
with(profile_completed_image_view) {
visible()
text = state.toString()
}
}
})
}
}
}
But the problem is if data fetching failed due to internet connectivity or any other server related problem loading indicator does not invisible that means it still loading though I make the loadingStatus false and error message is shown. it means .doOnTerminate { initialLoadingState.postValue(Event(loading(false))) } is not called if error occured. This is the first problem. Another problem is loadInitial() and loadAfter() is being called simultaneously at the first call. But I just want the loadInitial() method is called at the beginning. after scrolling loadAfter() method will be called.
Try replacing all your LiveData's postValue() methods by setValue() or simply .value =.
The problem is that the postValue() method is for updating the value from a background thread to observers in the main thread. In this case you are always changing the values from the main thread itself, so you should use .value =.
Hope it's not too late.