There is CustomWebViewClient with override function onPageFinished. What is the shortest way to notify MainViewModel about the function triggered? I mean some event.
I suppose that can use StateFlow, something like this:
class MainViewModel : ViewModel() {
init {
val client = CustomWebViewClient()
viewModelScope.launch {
client.onPageFinished.collect {
// ...
}
}
}
}
class CustomWebViewClient() : WebViewClient() {
private val _onPageFinished = MutableStateFlow("")
val onPageFinished = _onPageFinished.asStateFlow()
override fun onPageFinished(view: WebView, url: String) {
_onPageFinished.update { "" }
}
}
But in this case need to transfer unnecessary empty string and will be occurs first call before onPageFinished called because MutableStateFlow has value. So appear required add some enum or class in order to do filter with when keyword.
Maybe is there more shortest way to do that?
You can add lambda parameter into CustomWebViewClient constructor that will get called once page is finished.
class MainViewModel : ViewModel() {
init {
val client = CustomWebViewClient({handle the event})
}
}
class CustomWebViewClient(onPageFinished: () -> Unit) : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
onPageFinished()
}
}
Please note that referencing anything from android.* package in a ViewModel is most often a big no-go.
If you want to use the MutablStateFlow approach, another option is to also override onPageStarted as well and do something like
class CustomWebViewClient(): WebViewClient() {
private val _onPageFinished = MutableStateFlow(false)
val onPageFinished = _onPageFinished.asStateFlow()
override fun onPageStarted(
view: WebView?,
url: String?,
favicon: Bitmap?,
) {
_onPageFinished.update { false }
}
override fun onPageFinished(view: WebView?, url: String?) {
_onPageFinished.update { true }
}
}
class MainViewModel : ViewModel() {
init {
val client = CustomWebViewClient()
viewModelScope.launch {
client.onPageFinished.collect {
if (it) {
// Do stuff when page is loaded
} else {
// Do stuff when page starts loading
}
}
}
}
}
Though ultimately using flows for this is kinda overkill and using the lambda approach suggested by Mieszko Koźma is probably more straight forward.
Related
I have a specific UseCase where initialize app data. I store every <reference, listener> in a dispatchListeners list to unsubscribe later.
typealias EventListener = Pair<DatabaseReference, ValueEventListener>
class InitAppDataUseCase(
private val subscribeUserUseCase: SubscribeUserUseCase,
private val subscribeNewsUseCase: SubscribeNewsUseCase,
private val subscribeStoriesUseCase: SubscribeStoriesUseCase,
private val subscribeMeetingsUseCase: SubscribeMeetingsUseCase,
private val subscribeCategoriesUseCase: SubscribeCategoriesUseCase,
private val dispatchers: AppDispatchers
): UseCase<Unit, Unit> {
private val dispatchListeners = mutableListOf<EventListener>()
override suspend fun execute(input: Unit) {
init()
}
private fun EventListener.add() = dispatchListeners.add(this)
private suspend fun init() = CoroutineScope(dispatchers.io).launch {
runCatching {
listOf(
async { subscribeUserUseCase.execute().add() },
async { subscribeNewsUseCase.execute().add() },
async { subscribeStoriesUseCase.execute().add() },
async { subscribeMeetingsUseCase.execute().add() },
async { subscribeCategoriesUseCase.execute().add() }
).awaitAll()
}
}
fun clearSubscribed() = CoroutineScope(dispatchers.io).launch {
dispatchListeners.forEach { referenceToListener ->
with(referenceToListener) {
first.removeEventListener(second)
}
}
}
}
But where should I unsubscribe?
When the user remove an account or sign out from my app, I do this in specific ViewModel and redirect him to AuthScreen after this executed.
But what should I do if user just close my app? Is this correct way to unsubscribe in onDestroy() of my MainActivity? I have doubts because clearSubscribed() is a heavy operation. Am I right if the user have a poor internet connection and - this operation couldn't be executed because applicationScope will be dead?
class MainActivity : ComponentActivity() {
private val initAppDataUseCase by inject<InitAppDataUseCase>()
override fun onCreate() {}
override fun onDestroy() {
super.onDestroy()
initAppDataUseCase.clearSubscribed()
}
}
You have to remove the listener according to the life cycle of your activity. Since you're using Kotlin, most likely in an MVVM architecture, I would rather use callbackFlow. There is a very helpful example in the documentation. However, in the case of Firebase, to attach and dettach the listener, please use the following lines of code:
override fun getDataFromRealtimeDatabase() = callbackFlow {
val listener = object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
//Do what you need to do with the data.
}
override fun onCancelled(e: DatabaseError) {
Log.d("TAG", "${e?.message}") //Never ignore potential errors!
}
}
yourRef.addValueEventListener(listener) //Attach the listener.
awaitClose {
yourRef.removeEventListener(listener) //Dettach the listener.
}
}
I have an activity where I setup an animated AnimatedVector on an ImageView then I am loading an url in a WebView, everythings good right here.
The issue is in onPageStarted webview client callback, I got a crash because binding.loader.drawable return null so the cast is impossible.
I can't figure it out why the drawable is null here !
Second issue is (if i comment the line in onPageStarted) in onPageFinished, the two visibility of my views I try to set does nothing at all, they are still visibles.
Spoiler : Of course the app crash right after when trying to get the drawable and cast it
Have you already face this issue ?
class ViewRecipeActivity : AppCompatActivity() {
private val binding by viewBinding(ActivityViewRecipeBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupUI()
}
//region setup UI
private fun setupUI() {
setUpLoader()
setupWebView()
}
private fun setUpLoader() {
with(binding.loader) {
val drawable = AnimatedVectorDrawableCompat.create(this#ViewRecipeActivity, R.drawable.animated_loader)
setImageDrawable(drawable)
}
}
private fun setupWebView() {
val client = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
(binding.loader.drawable as Animatable).start() //Crash here because drawable is null
}
override fun onPageFinished(view: WebView?, url: String?) {
binding.loader.visibility = View.GONE
binding.loaderBackground.visibility = View.GONE
(binding.loader.drawable as Animatable).stop()
}
}
with(binding.recipeView) {
webViewClient = client
}
val recipeUrl = intent.extras?.getString(RECIPE_URL_EXTRA)
if(recipeUrl == null) {
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
} else {
binding.recipeView.loadUrl(recipeUrl)
}
}
//endregion
companion object {
const val RECIPE_URL_EXTRA = "recipe_url_extra"
}
}
I have created a Generic Fragment class to handle all type of responses from server. I want to do some sort of DataTableProvider<*> to hanle any type of response.
How could I achieve this.
class TestFragmentActivity : AppCompatActivity(), DataTableProvider<Any> {
protected val mTabPatientsFragment = TabPatientsFragment()
protected val mTabObservationsFragment = TabObservationsFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_fragment)
replaceFragment()
}
private fun replaceFragment(){
supportFragmentManager.beginTransaction().replace(R.id.frame_container,
mTabPatientsFragment).commit()
}
override fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<Any> {
val dataTableId = mTableFragment.dataTableId
if (dataTableId.equals("observations"))
return mTabObservationsFragment
else return mTabPatientsFragment
}
override fun getDataTableConfig(mTableFragment: DataTableFragment<Any>): DataTableConfig {
val dataTableId = mTableFragment.dataTableId
val config = DataTableConfig()
config.noRecordCell = R.layout.cell_no_record
config.showSearch = false
config.showAddButton = false
if (dataTableId.equals("observations"))
{
config.cellResourceId = R.layout.home_observation_cell
} else config.cellResourceId = R.layout.home_patient_cell
return config
}
}
getDataTableListener callback in above fragment has error type mismatch required DataTableListener found TabObservationFragment
TabObservationFragment
class TabObservationFragment : AppBaseFragment(),DataTableListener<Observation>
TabPatientFragment
class TabPatientFragment : AppBaseFragment(),DataTableListener<Patient>
How could I set it to work for all type of responses.
I tried DataTableListener<*> but could not achieve
The error states
projections are not allowed for immediate arguments of a supertype
How could I use DataTableProvider<*> to work for all type of responses
Edit
I have couple of fragment with fragmentViewpager inside TestFragmentActivity .
I have got a structure that helps to implement pagination ,search and implement everything in a fragment. But according to that structure DataTableProvider must be handle in activity and basis of tableId I updated callback of getDataTableListener and getDataTableListener
The above callback should return some type of
Is there a way to achieve callback like below
override fun getDataTableConfig(mTableFragment: DataTableFragment<*>?): DataTableConfig?
override fun getDataTableListener(mTableFragment: DataTableFragment<*>?): DataTableListener<*>?
Quick answer, use "out" modifier:
fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<out Any>
Long answer:
What you are looking for is variance, which can you read about in official kotlin docs:
https://kotlinlang.org/docs/reference/generics.html
Because for example List interface looks like this:
public interface List<out E>
You can do assigement like this:
val list: List<Any> = listOf(1,2,3)
But it is not possible to do:
val mutableList : MutableList<Any> = listOf(1,2,3)
Because MutableList doesn't have "out" modifier. It makes sense, because MutableList can be changed, to MutableList you could add for example String, but it already points to List.
In your example you can use "out" modifier, if, and only if, your DataTableListener doesn't use generic type as input. For example:
interface DataTableListener<T>{
fun assignValue(t:T)
fun getValue():T
}
With interface like above, you still could use "out" modifier in your function, but you won't be able to execute "assignValue" function.
Whole example:
class Patient
class Observation
class DataTableFragment<T>
interface DataTableListener<T> {
fun assignValue(t: T)
fun getValue(): T
}
class TabObservationFragment : DataTableListener<Observation> {
override fun getValue(): Observation {
TODO("Not yet implemented")
}
override fun assignValue(t: Observation) {
TODO("Not yet implemented")
}
}
class TabPatientFragment : DataTableListener<Patient> {
override fun getValue(): Patient {
}
override fun assignValue(t: Patient) {
TODO("Not yet implemented")
}
}
val mTabObservationsFragment = TabObservationFragment()
val mTabPatientsFragment = TabPatientFragment()
fun getDataTableListener(mTableFragment: DataTableFragment<Any>): DataTableListener<out Any> {
val test = "observations"
if (test == "observations")
return mTabObservationsFragment
else return mTabPatientsFragment
}
fun getIt() {
val listener = getDataTableListener(DataTableFragment())
listener.assignValue("test")
}
I am trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.
I am looking for a way of completion block for my kotlin code. In Swift i have my function:
func fetchRegister(with request: RegisterRequest, completion: #escaping (Result<RegisterResponse,DataResponseError>) -> Void) {
//do some stuff
// if i got error i can use completion(Result.failure(DataResponseError.networking))
}
in kotlin my current code is:
fun fetchRegister(withRequest: RegisterRequest, callback: (Result<RegisterResponse,DataResponseError>) -> Unit) {
//do some stuff
//cant use callback.onFailure(DataResponseError.networking)
}
My result interface:
interface Result<T,U: DataResponseError> {
fun onSuccess(data: T)
fun onFailure(Error: U)
}
and my DataResponseError:
enum class DataResponseError(val errorMessage: String) {
httpBody("An error occured while creating httpBody"),
token("An error occured while getting token"),
networking("An error occured while fetching data"),
decoding("An error occured while decoding data")
}
at this moment data cant get out from this function , i cant use callback.onfailure or onSuccess with passing data. How can i fix it?
There are two ways of implementing callbacks in kotlin.
You can have some interface and pass the reference of interface from actvity to the viewModel or the adapter and then you can call specific function from there. Example: Interface:
interface CompletionHandler {
fun onSuccess(data: SomeClass)
fun onFailure(error: String)
}
Class:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.fetchData(this)
}
fun onSuccess(data: SomeClass) {
//onSuccess
}
fun onFailure(error: String) {
//onFailure
}
}
ViewModel / Adapter class:
class MainViewModel(): ViewModel() {
fun fetchData(completion: CompletionHandler) {
//Logic
completion.onSuccess(responseData)
}
}
Just like in IOS(swift) we can also use anonymous functions for callback into activities.Example:Class:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.fetchData(data) { data ->
//Logic
}
}
}
ViewModel / Adapter class:
class MainViewModel(): ViewModel() {
fun fetchData(data: String, completion: (SomeClass) -> Unit) {
//Logic
completion(responseData)
}
}
Replace your fetchRegister function with:
fun fetchRegister(withRequest: RegisterRequest, callback: Result<RegisterResponse,DataResponseError>){
//In case of success
callback.onSuccess(data)
// In case of failure
callback.onFailure(DataResponseError.networking)
}
val callback = object : Result<RegisterResponse,DataResponseError> {
override fun onSuccess(response: RegisterResponse) {
// Do Something
}
override fun onFailure(error: DataResponseError) {
// Do Something
}
}
fetchRegister(request, callback)