Как обновить представление из пользовательского источника данных после запроса asyn c ohttp - PullRequest
0 голосов
/ 15 января 2020

Я использую пример Google Dagger 2 для создания собственного приложения. Я хочу отображать данные в RecyclerViev, для этого мне нужно загрузить асинхронные данные с удаленного сервера. Я не знаю, как обновить представление после получения ответа от сервера

Фрагмент

class RegionsFragment : DaggerFragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private val viewModel by viewModels<RegionsViewModel> { viewModelFactory }

    private lateinit var viewDataBinding: FragmentRegionsBinding

    private lateinit var listAdapter : RegionsAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewDataBinding = FragmentRegionsBinding.inflate(inflater, container, false).apply {
            viewmodel = viewModel
        }

        return viewDataBinding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // Set the lifecycle owner to the lifecycle of the view
        viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
        setupListAdapter()

        viewModel.loadRegions(true)
    }


    private fun setupListAdapter() {
        val viewModel = viewDataBinding.viewmodel
        if (viewModel != null) {
            listAdapter = RegionsAdapter(viewModel)
            viewDataBinding.regionsList.adapter = listAdapter

            listAdapter.onItemClick = {region ->
                val intent = Intent(context, BudgetsActivity::class.java)
                intent.putExtra("id_region", region.id)
                startActivity(intent)
            }
        } else {
            //Timber.w("ViewModel not initialized when attempting to set up adapter.")
        }
    }
}

ViewModel

class RegionsViewModel @Inject constructor(
    private val regionsRepository: RegionsRepository
) : ViewModel() {

    private val _items = MutableLiveData<List<Region>>().apply { value = emptyList() }
    val items: LiveData<List<Region>> = _items

    private val _dataLoading = MutableLiveData<Boolean>()
    val dataLoading: LiveData<Boolean> = _dataLoading

    private val isDataLoadingError = MutableLiveData<Boolean>()

    init {
        loadRegions(true)
    }

    fun loadRegions(forceUpdate: Boolean) {
        _dataLoading.value = true

        wrapEspressoIdlingResource {

            viewModelScope.launch {
                val regionsResult = regionsRepository.getRegions(forceUpdate)

                if (regionsResult is Result.Success) {
                    val regions = regionsResult.data

                    val regionsToShow = ArrayList<Region>()
                    for (region in regions){
                        regionsToShow.add(region)
                    }
                    isDataLoadingError.value = false
                    _items.value = ArrayList(regionsToShow)
                } else {
                    isDataLoadingError.value = false
                    _items.value = emptyList()
                    //showSnackbarMessage(R.string.loading_tasks_error)
                }

                _dataLoading.value = false
            }
        }
    }
}

ListAdapter

class RegionsAdapter(private val viewModel: RegionsViewModel): ListAdapter<Region, ViewHolder>(TaskDiffCallback()) {

    var onItemClick: ((Region) -> Unit)? = null

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)

        holder.itemView.setOnClickListener {
            onItemClick?.invoke(item);
        }

        holder.bind(viewModel, item)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    class ViewHolder private constructor(private val binding: RegionItemBinding) : RecyclerView.ViewHolder(binding.root) {

            fun bind(viewModel: RegionsViewModel, item: Region) {

                binding.viewmodel = viewModel
                binding.region = item
                binding.executePendingBindings()
            }

            companion object {
                fun from(parent: ViewGroup): ViewHolder {
                    val layoutInflater = LayoutInflater.from(parent.context)
                    val binding = RegionItemBinding.inflate(layoutInflater, parent, false)

                    return ViewHolder(binding)
                }
            }
    }
}

class TaskDiffCallback : DiffUtil.ItemCallback<Region>() {
    override fun areItemsTheSame(oldItem: Region, newItem: Region): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Region, newItem: Region): Boolean {
        return oldItem == newItem
    }
}

Репозиторий

class DefaultRegionsRepository @Inject constructor(
    @RegionsRemoteDataSource private val regionsRemoteDataSource: RegionsDataSource,
    @RegionsLocalDataSource private val regionsLocalDataSource: RegionsDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : RegionsRepository {

    private var cachedRegions: ConcurrentMap<Int, Region>? = null

    override suspend fun getRegions(forceUpdate: Boolean): Result<List<Region>> {
        wrapEspressoIdlingResource {
            return withContext(ioDispatcher) {
                // Respond immediately with cache if available and not dirty
                if (!forceUpdate) {
                    cachedRegions?.let { cachedTasks ->
                        return@withContext Success(cachedTasks.values.sortedBy { it.name })
                    }
                }

                val newRegions = fetchRegionsFromRemoteOrLocal(forceUpdate)

                // Refresh the cache with the new tasks
                (newRegions as? Success)?.let { refreshCache(it.data) }

                cachedRegions?.values?.let { tasks ->
                    return@withContext Success(tasks.sortedBy { it.name })
                }

                (newRegions as? Success)?.let {
                    if (it.data.isEmpty()) {
                        return@withContext Success(it.data)
                    }
                }

                return@withContext Error(Exception("Illegal state"))
            }
        }
    }

    private suspend fun fetchRegionsFromRemoteOrLocal(forceUpdate: Boolean): Result<List<Region>> {
        // Remote first
        val remoteRegions = regionsRemoteDataSource.getRegions()
        when (remoteRegions) {
            //is Error -> Timber.w("Remote data source fetch failed")
            is Success -> {
                refreshLocalDataSource(remoteRegions.data)
                return remoteRegions
            }
            else -> throw IllegalStateException()
        }

        // Don't read from local if it's forced
        if (forceUpdate) {
            return Error(Exception("Can't force refresh: remote data source is unavailable"))
        }

        // Local if remote fails
        val localRegions = regionsLocalDataSource.getRegions()
        if (localRegions is Success) return localRegions
        return Error(Exception("Error fetching from remote and local"))
    }

    private fun refreshCache(tasks: List<Region>) {
        cachedRegions?.clear()
        tasks.sortedBy { it.id }.forEach {
            cacheAndPerform(it) {}
        }
    }

    private suspend fun refreshLocalDataSource(regions: List<Region>) {
        //regionsLocalDataSource.deleteAllTasks()
        for (region in regions) {
            //regionsLocalDataSource.saveTask(region)
        }
    }

    private fun cacheRegion(region: Region): Region {
        val cachedRegion = Region(region.id, region.name, region.deptAmount, region.accrualAmount)
        // Create if it doesn't exist.
        if (cachedRegions == null) {
            cachedRegions = ConcurrentHashMap()
        }
        cachedRegions?.put(cachedRegion.id, cachedRegion)
        return cachedRegion
    }

    private inline fun cacheAndPerform(task: Region, perform: (Region) -> Unit) {
        val cachedTask = cacheRegion(task)
        perform(cachedTask)
    }
}

DataSource - думаю, мне нужно обновить представление из источника данных

object RegionsRemoteDataSource :
    RegionsDataSource {
    private const val SERVICE_LATENCY_IN_MILLIS = 2000L

    private var REGIONS_SERVICE_DATA = LinkedHashMap<Int, Region>(2)

    private val client = OkHttpClient()
    val url = "http://.."

    override suspend fun getRegions(): Result<List<Region>> {
        val request = Request.Builder().url(url).build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                e.printStackTrace()
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (!response.isSuccessful) throw IOException("Unexpected code $response")

                    val gson = Gson()
                    val  regionList: List<RegionRemote> = gson.fromJson(response.body?.string(), Array<RegionRemote>::class.java).toList()

                    val sortedRegionList = regionList.sortedWith(compareBy({ it.name }, { it.name }))

                    for(region in sortedRegionList) {
                        addRegion(
                            region.id,
                            region.name,
                            region.deptAmount + " тыс.р.",
                            region.accrualAmount + " тыс.р."
                        )
                    }
                    Update UI How??
                }
            }
        })

        /*client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("Unexpected code $response")

            val gson = Gson()
            val  regionList: List<RegionRemote> = gson.fromJson(response.body?.string(), Array<RegionRemote>::class.java).toList()

            val sortedRegionList = regionList.sortedWith(compareBy({ it.name }, { it.name }))

            for(region in sortedRegionList) {
                addRegion(
                    region.id,
                    region.name,
                    region.deptAmount + " тыс.р.",
                    region.accrualAmount + " тыс.р."
                )
            }

            println(response.body!!.string())
        }*/

        val regions = REGIONS_SERVICE_DATA.values.toList()
        delay(SERVICE_LATENCY_IN_MILLIS)
        return Success(regions)
    }

    private fun addRegion(id: Int, name: String, deptAmount: String, accrualAmount: String) {
        val newRegion = Region(id, name, deptAmount, accrualAmount)
        REGIONS_SERVICE_DATA.put(newRegion.id, newRegion)
    }
}
...