StateFlow collect does not change the data correctly - android

I'm using MVI architecture, coroutine and flow,
when I receive the data from the API, the status changed from LOADING to SUCCESS, and when I collect the stateFlow variable it submit the recyclerView successfully while when I try to hide the loading view (progressBar, lottie) the view freeze for a moment and it does not disappear.
I tried two ways
I tried to use the stateFlow in XML like this: android:visibility="#{viewModel.writersFlow.status == Results.Status.LOADING ? View.VISIBLE : View.GONE}", and of course I put lifecycleOwner = viewLifecycleOwner in onViewCreated function and I passed the viewModel to the XML
change the the visibility programmatically like bellow:
Repository:
override suspend fun getWriters(): Flow<Results<BaseModel<WriterModel>?>> =
resultFlowData(
networkCall = {
remoteDataSource.getResult {
endpoints.getWriters()
}
}
)
ViewModel:
private val _writersFlow: MutableStateFlow<Results<BaseModel<WriterModel>?>> =
MutableStateFlow(start())
val writersFlow: StateFlow<Results<BaseModel<WriterModel>?>>
get() = _writersFlow.asStateFlow()
private fun fetchWriters() {
viewModelScope.launch(Dispatchers.IO) {
writerRepository.getWriters().collect {
"writers: $it".log()
_writersFlow.emit(it)
}
}
}
Fragment: here in fragment you will see the binding.loading.visibility = View.GONE in both cases (SUCCESS, ERROR)
private fun gettingWriters() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.writersFlow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
when (it.status) {
Results.Status.SUCCESS -> {
writersAdapter.submitList(it.data?.response)
binding.loading.visibility = View.GONE
}
Results.Status.ERROR -> {
"$HOME_FRAGMENT: ${it.status}, ${it.code}, ${it.message}".log()
if (!it.message.isNullOrEmpty()) binding.root.snack(it.message ?: "") {}
binding.loading.visibility = View.GONE
}
Results.Status.LOADING -> {
binding.loading.visibility = View.VISIBLE
}
else -> {}
}
}
}
}
In the first way the loading view does not even appear but in the second way it appears but never disappear.

Related

Kotlin recycler view cannot change background when later calling a function

I'm new to Kotlin and so if you do find rubbish code here and poor practices, do let me know. Otherwise, here is the issue I am having.
I am writing a tiny app that presents users with multiple questions from which they have to select the correct answer. If they select the correct answer, the option is supposed to be highlighted green for 250ms and then they move on to the next question. Otherwise, select the incorrect answer. The logic for moving onto the next question is defined in the main activity, and the background change logic is defined in the adapter class. Below is what the adapter class looks like at the moment (I've only included that which I think is relevant just to add too much faff):
class QuestionOptionAdapter(
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener {
if (item == correctAnswer) {
runBlocking {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_correct)
delay(250)
}
onSelectedCorrectAnswer()
} else {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_incorrect)
onSelectedIncorrectAnswer()
}
}
}
}
I realised that although the code to changes the background is executed before onSelectedCorrectAnswer(), it won't change the background colour until the entire block has finished executing. Therefore, the user never sees the updated background.
Is there a way to show an update before the block finishes executing?
runBlocking doesn't work because it blocks. It will just wait for the whole time you delay and block the main thread so the device will be frozen and not show any visual changes until it returns.
You need to pass the Activity or Fragment's CoroutineScope into the adapter for the adapter to use. You can then launch a coroutine that won't block the main thread when you delay inside it.
Here I lifted the coroutine to encompass all your click listener logic. That will make it easier to modify the behavior later if you want.
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
delay(250)
onSelectedCorrectAnswer()
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}
Actually, you probably want to also prevent the user from clicking other options during that 250ms delay, so you should set a Boolean that can disable further clicking of items during the delay:
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
private var isLockClickListeners = false
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
if (isLockClickListeners) {
return#setOnClickListener
}
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
isLockClickListeners = true
delay(250)
onSelectedCorrectAnswer()
isLockClickListeners = false
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}

Jetpack compose lazy column not recomposing with list

I have a list which is stored inside a Viewmodel via Stateflow.
class FirstSettingViewModel : ViewModel() {
private val _mRoomList = MutableStateFlow<List<InitRoom>>(mutableListOf())
val mRoomList: StateFlow<List<InitRoom>> = _mRoomList
...
I observe the flow via collectAsState(). The LazyColumn consists of Boxes which can be clicked.
val roomList = mViewModel.mRoomList.collectAsState()
Dialog {
...
LazyColumn(...) {
items(roomList.value, key = { room -> room.room_seq}) { room ->
Box(Modifier.clickable {
**mViewModel.selectItem(room)**
}) {...}
}
}
}
When a click event occurs, the viewModel changes the 'isSelected' value via a copied list like this.
fun selectItem(room: InitRoom) = viewModelScope.launch(Dispatchers.IO) {
try {
val cpy = mutableListOf<InitRoom>()
mRoomList.value.forEach {
cpy.add(it.copy())
}
cpy.forEach {
it.isSelected = it.room_seq == room.room_seq
}
_mRoomList.emit(cpy)
} catch (e: Exception) {
ErrorController.showError(e)
}
}
When in an xml based view and a ListAdapter, this code will work well, but in the above compose code, it doesn't seem to recompose the LazyColumn at all. What can I do to re-compose the LazyColumn?
Use a SnapshotStateList instead of an ordinary List
change this,
private val _mRoomList = MutableStateFlow<List<InitRoom>>(mutableListOf())
val mRoomList: StateFlow<List<InitRoom>> = _mRoomList
to this
private val _mRoomList = MutableStateFlow<SnapshotStateList<InitRoom>>(mutableStateListOf())
val mRoomList: StateFlow<SnapshotStateList<InitRoom>> = _mRoomList

Problem Implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview : adapter Recyclerview not update

I'm newbie use Kotlin on my dev apps android,
and now, I on step learn to implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview. The My Apps:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
I call function to get data from server on onCreateView HomeFragment, and observe this with show shimmer data on my Recylerview when is loading, try update RecyclerView when success, and show view Failed Load -button refresh when error.
The problem is:
Adapter Recyclerview not Update when success get Data from Server. Adapter still show shimmer data
With case error (no Internet), i show view Failed Load, with button refresh. Tap to refresh, i re-call function to get data server, but fuction not work correct. Recyclerview show last data, not show Failed Load again.
Bellow my code
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
NewsAdapter
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
I hope someone can help me to solve the problem. thanks, sorry for my English.
You have 3 different LiveDatas, right? One with a list of news data, one with a loading error message, and one with a loading state.
You set up an Observer for each of these, and that observer function gets called whenever the LiveData's value updates. That's important, because here's what happens when your request succeeds, and you get some new data:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
which means newsList updates, then loadError updates, then loading updates.
So your observer functions for each of those LiveDatas run, in that order. But you're not testing the new values (except for null checks), so the code in each one always runs when the value updates. So when your response is successful, this happens:
newsList observer runs, displays as successful, calls getDataNews
loadError observer runs, value is null so nothing happens
loading observer runs, value is false but isn't checked, displays as loading, calls getDataNewsShimmer
So even when the response is successful, the last thing you do is display the loading state
You need to check the state (like loading) before you try to display it. But if you check that loading is true, you'll have a bug in fetchNews - that sets loading = true, starts a coroutine that finishes later, and then immediately sets loading = false.
I'd recommend trying to create a class that represents different states, with a single LiveData that represents the current state. Something like this:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
And then you just need to observe that state:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
You also might want to break that display code out into separate functions (like showError(), showList(state.newsList) etc) and call those from the branches of the when, if that makes it more readable
I hope that makes sense! When you have a single value representing a state, it's a lot easier to work with - set the current state as things change, and make your observer handle each possible UI state by updating the display. When it's Loading, make it look like this. When there's an Error, make it look like this
That should help avoid bugs where you update multiple times for multiple things, trying to coordinate everything. I'm not sure why you're seeing that problem when you reload after an error, but doing this might help fix it (or make it easier to see what's causing it)

What to return in this recursive method?

I'm creating a method to recursively search for a View inside an ArrayList. It will loop through this ArrayList and, if it contains an ArrayList, it will be searched for Views too, and so on, until finding a View to return. This is so I can make whatever View is inside there invisible.
fun searchForView(arrayList: ArrayList<*>): View {
arrayList.forEach { item ->
if (item is View) {
return item
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
} // Error here, needs to return a View
So I will use it like this:
someArrayList.forEach {
searchForView(someArrayList).visibility = View.INVISIBLE
}
However it is giving me an error because there needs to be a return someView statement near the end of the method. Whenever I call it, the ArrayList being searched will always have a View. So what should I be returning here at the end, knowing that whatever View found will already be returned?
You can set inside function and don't return anything
fun searchForView(arrayList: ArrayList<*>){
arrayList.forEach { item ->
if (item is View) {
item.visibility = View.INVISIBLE // set here
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
}
You should use searchForView(item) instead of item.forEach { searchForView(it as ArrayList<*>) } as #IR42 suggested since you don't know each item in arraylist is an arraylist or not.
Your function is not compileable because it's supposed to return a View, but you aren't returning a View in the else branch or if you reach the end of the input list without finding a View.
However, if all this function does is return a View, then it is not usable for your requirement to set all views' visibility. It would only return a single View.
Instead, you can pass a function argument for what to do to each view it finds. There's no need to return anything.
fun ArrayList<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) when (item) {
is View -> block(item)
is ArrayList<*> -> item.forEachViewDeep(block)
}
}
And use it like:
someArrayList.forEachViewDeep {
it.visibility = View.INVISIBLE
}
If it's very deeply nested, you might want to rearrange this function to be tail-recursive like this:
tailrec fun List<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) {
if (item is View)
block(item)
}
filterIsInstance<ArrayList<*>>().flatten().forEachViewDeep(block)
}
I was trying to do the same thing like you before
and this is what I've made
class VisibilitySwitcher(private val mutableViewSet: MutableSet<View?>, private val onCondition: Boolean = true){
fun betweenVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenVisibleOrInvisible(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenInVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.INVISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.INVISIBLE}
}
}
}
}
Usage Example
class LoginActivity : BaseActivity() {
#Inject
#ViewModelInjection
lateinit var viewModel: LoginVM
private lateinit var mutableViewSet: MutableSet<View?>
override fun layoutRes() = R.layout.activity_login
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
facebookBtn.setOnClickListener { handleClickEvent(it) }
googleBtn.setOnClickListener { handleClickEvent(it) }
}
private fun handleClickEvent(view: View) {
when (view) {
facebookBtn -> { viewModel.smartLoginManager.onFacebookLoginClick() }
googleBtn -> { viewModel.smartLoginManager.onGoogleLoginClick() }
}
mutableViewSet = mutableSetOf(facebookBtn, googleBtn, progressBar)
VisibilitySwitcher(mutableViewSet).betweenVisibleOrGone() // <----- Use without Condition
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
VisibilitySwitcher(mutableViewSet, resultCode != -1).betweenVisibleOrGone() //<-- Use with Conditions
viewModel.smartLoginManager.onActivityResultCallBack(requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
}
The point is whenever you click login from facebook or google button
It will set visibility for facebook and google to be gone and set progressbar(the default of progressbar is View.GONE) to be visible
At override fun onActivityResult()
if the resultcode is not -1 it means that it got some error or cancel
so it will switch back the progressbar to be gone and change facebook and google button to be visible again
If you want to fix your own code I would do this
fun searchForView(mutableViewSet: MutableSet<View?>){
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE} //<-- you can delete this if you don't want
}
}
}
Or very short form
fun searchForView(mutableViewSet: MutableSet<View?>) = mutableViewSet.forEach { when (it?.visibility) {View.VISIBLE -> it.visibility = View.INVISIBLE } }
Usage
val mutableViewSet = mutableSetOf(your view1,2,3....)
searchForView(mutableViewSet)
if it has to use arrayList: ArrayList<*> Then
fun searchForView(arrayList: ArrayList<*>) = arrayList.forEach{ if (it is View) it.visibility = View.INVISIBLE

Turning listeners into kotlin coroutine channels

I have several functions that I want to use to do pipelines with Channels. The main one is globalLayouts, where I create a Channel from the framework listener:
fun View.globalLayouts(): ReceiveChannel<View> =
Channel<View>().apply {
val view = this#globalLayouts
val listener = ViewTreeObserver.OnGlobalLayoutListener {
offer(view)
}
invokeOnClose {
viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
viewTreeObserver.addOnGlobalLayoutListener(listener)
}
#UseExperimental(InternalCoroutinesApi::class)
fun <E> ReceiveChannel<E>.distinctUntilChanged(context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel<E> =
GlobalScope.produce(context, onCompletion = consumes()) {
var last: Any? = Any()
consumeEach {
if (it != last) {
send(it)
last = it
}
}
}
fun View.keyboardVisibility(): ReceiveChannel<KeyboardVisibility> {
val rect = Rect()
return globalLayouts()
.map {
getWindowVisibleDisplayFrame(rect)
when (rect.height()) {
height -> KeyboardVisibility.HIDDEN
else -> KeyboardVisibility.SHOWN
}
}
.distinctUntilChanged()
}
I have a CoroutineScope called alive:
val ControllerLifecycle.alive: CoroutineScope
get() {
val scope = MainScope()
addLifecycleListener(object : Controller.LifecycleListener() {
override fun preDestroyView(controller: Controller, view: View) {
removeLifecycleListener(this)
scope.cancel()
}
})
return scope
}
then I do:
alive.launch {
root.keyboardVisibility().consumeEach {
appbar.setExpanded(it == KeyboardVisibility.HIDDEN)
}
}
This code starts working just fine, but I get
kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelled}#811031f
once my alive scope is destroyed. Right after invokeOnClose is called in globalLayouts. What am I doing wrong and how do I debug this?
Figured it out - the code works fine, but
viewTreeObserver.removeOnGlobalLayoutListener(listener)
is bugged for CoordinatorLayout.

Categories

Resources