I am trying to implement MVI Architecture in Android, but don't want to use Mosby Library. I want to learn the basics first.
I am building a sample app where when I press a button, text in the textview changes(initially the text is something else). Here is the code for MainActivity and MainPresenter.
class MainActivity : AppCompatActivity(), MainContract.View {
lateinit var mPresenter: MainContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPresenter = MainPresenter()
mPresenter.attachPresenter(this)
bind()
}
#SuppressLint("CheckResult")
private fun bind() {
mPresenter.states().subscribe({ state ->
render(state)
}, {
Log.e("error", "Error is: ", it)
it.printStackTrace()
})
mPresenter.addIntents(intents())
}
override fun intents(): Observable<MainIntent> {
return Observable.merge(
initialIntent(),
clickIntent()
)
}
override fun render(state: MainViewState) {
btn_show.isEnabled = state.isEnabledButton
helloWorldTextView.text = state.message
loadingIndicator.visibility = if (state.isLoading) View.VISIBLE else View.GONE
}
private fun initialIntent(): Observable<MainIntent.InitialIntent> = Observable.just(MainIntent.InitialIntent)
private fun clickIntent(): Observable<MainIntent.ClickIntent> {
return btn_show.clicks().map { MainIntent.ClickIntent("Eureka") }
}
}
class MainPresenter : MainContract.Presenter {
private val intentsSubject: PublishSubject<MainIntent> = PublishSubject.create()
override fun states(): Observable<MainViewState> {
return statesObservable
}
private lateinit var view: MainContract.View
override fun attachPresenter(view: MainContract.View) {
this.view = view
}
#SuppressLint("CheckResult")
override fun addIntents(intents: Observable<MainIntent>) {
intents.subscribe(intentsSubject)
}
private val reducer =
BiFunction { previousState: MainViewState, result: MainResult ->
when (result) {
is MainResult.InitialResult.InFlight -> previousState.copy(
isLoading = true,
message = "Initial Result",
isEnabledButton = false
)
is MainResult.InitialResult.Success -> previousState.copy(
isLoading = true,
message = "Initial Success",
isEnabledButton = true
)
is MainResult.InitialResult.Error -> previousState.copy(
isLoading = false,
message = "Error Initially",
isEnabledButton = true
)
is MainResult.ClickedResult.Success -> previousState.copy(
isLoading = false,
message = System.currentTimeMillis().toString(),
isEnabledButton = true
)
is MainResult.ClickedResult.Error -> previousState.copy(
isLoading = false,
message = "Error Clicked",
isEnabledButton = true
)
is MainResult.ClickedResult.InFlight -> previousState.copy(
isLoading = true,
message = "Clicked In Flight",
isEnabledButton = false
)
}
}
private fun actionFromIntent(intent: MainIntent): MainAction {
if (intent is MainIntent.InitialIntent) {
return MainAction.InitialAction
} else if (intent is MainIntent.ClickIntent) {
return MainAction.ClickedAction("Hello")
} else {
return MainAction.InitialAction
}
}
private var actionProcessor: ObservableTransformer<MainAction, MainResult> = ObservableTransformer { actions ->
actions.publish { shared ->
Observable.merge<MainResult>(
shared.ofType(MainAction.InitialAction::class.java).compose(initialActionProcessor),
shared.ofType(MainAction.ClickedAction::class.java).compose(clickActionProcessor)
)
}
}
private val initialActionProcessor =
ObservableTransformer<MainAction.InitialAction, MainResult.InitialResult> { action: Observable<MainAction.InitialAction> ->
action.switchMap {
Observable.just("hello initially")
.map { MainResult.InitialResult.Success(it) }
.cast(MainResult.InitialResult::class.java)
.onErrorReturn { MainResult.InitialResult.Error(it.message!!) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.InitialResult.InFlight }
}
}
private val clickActionProcessor =
ObservableTransformer<MainAction.ClickedAction, MainResult.ClickedResult> { action: Observable<MainAction.ClickedAction> ->
Observable.just("Click").map { message ->
MainResult.ClickedResult.Success(message)
}.cast(MainResult.ClickedResult::class.java)
.onErrorReturn { MainResult.ClickedResult.Error("Error") }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.ClickedResult.InFlight }
}
private val statesObservable: Observable<MainViewState> = compose()
private fun compose(): Observable<MainViewState> {
return intentsSubject
.map {
actionFromIntent(it)
}
.compose(actionProcessor)
.scan(MainViewState.idle(), reducer)
.distinctUntilChanged()
.replay(1)
.autoConnect(0)
}
}
Problem is that only the Inital event is fired and nothing else. The code doesn't respond to clicks, render is called only initially once.
Also, if I remove the startWith{} from the actionProcessors code responds to clicks, but only once. After that, nothing happens.
Does anyone see issue with the code? I have been trying to get my head around this problem for a while now.
My previous reply:
It's not straight answer to your question. But if you implement what's below, you probably won't have the problem you actually asked about and you'll have easier MVI solution.
You probably try to merge https://speakerdeck.com/jakewharton/the-state-of-managing-state-with-rxjava-devoxx-us-2017, http://hannesdorfmann.com/android/mosby3-mvi-1 and https://medium.com/#oldergod/managing-state-with-rxjava-b0798a6c5757 ideas.
Take a look here: https://proandroiddev.com/taming-state-in-android-with-elm-architecture-and-kotlin-part-1-566caae0f706 - it's simpler. Part 1 and 2 should be enough.
I tried the 1st approach and was repulsed by initial complexity. In 2nd approach you don't have Action, Intent, Result, but Msg instead. It's simpler to reason about.
There's also new MVI course - but haven't checked it yet.
Current approach:
I tried mentioned Elm Architecture, but it is not complete. There are at least 2 problems:
Only one request can get through queue at one moment. Some RxJava
should do the trick (groupBy with 2 streams: ui, background
probably).
parallel requests would update the same state, so you should differentiate DataStates inside your UiState. So different state for different part of UI.
Before writing actual fixes we realised, this is not the way to go ATM: announced Composables could do the MVI trick: smooth and precise data transition to specific parts of UI.
Disclaimer: moderator removed my answer which WAS actual answer. Even more, my answer moved to comment is cut down, which makes it look unfinished. That's why this post emerged once again. After you read it dear moderator, you can remove disclaimer, thanks :)
Related
if(isPlayerNearby) {
Text("Player $playerName is within range!")
Image(/*some image*/)
Button(onClick = { attack() }) {
Text(text = "ELIMINATE")
}
} else {
Text("No players nearby. Keep searching.")
Image(/*some OTHER image*/)
Button(onClick = { attack() }) { //This button should be DISABLED
Text(text = "ELIMINATE")
}
}
I have a boolean variable that becomes true if a player is within range, but I'm confused about how I can make the screen live update when this requirement is met. It should switch from disabled to enabled and vice-versa as an opposing player becomes in and out of range.
Here is the code we're using to set isPlayerNearby to true
private var endpointDiscoveryCallback: EndpointDiscoveryCallback = object :
EndpointDiscoveryCallback() {
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
toEndpointId = endpointId
// An endpoint was found. We request a connection to it.
Nearby.getConnectionsClient(context)
.requestConnection(getLocalUserName(), endpointId, connectionLifeCycleCallback)
.addOnSuccessListener {
run {
endpointFound()
//This is where we will set isPlayerNearby to true
Toast.makeText(applicationContext, endpointId, Toast.LENGTH_SHORT).show()
}
}
.addOnFailureListener { _ ->
// Toast.makeText(applicationContext, "Could Not Connect", Toast.LENGTH_SHORT).show()
}
}
override fun onEndpointLost(endpointId: String) {
Toast.makeText(applicationContext, "Endpoint Lost", Toast.LENGTH_SHORT).show()
}
}
Setting the boolean to false will change the screen, but it won't live update - the screen basically needs to be rebuilt to see the results. I believe we have to use states but I am not too familiar.
use LiveData or StateFlow
val isPlayerNearby = MutableLiveData(false)
...
run {
endpointFound()
isPlayerNerby.postValue(true)
Toast.makeText(applicationContext,endpointId,Toast.LENGTH_SHORT)
.show()
}
in Fragment observe our liveData:
viewModel.isPlayerNearby.observe(viewLifecicleOwner) { isPlayerNearby ->
if(isPlayerNearby) {
Text("Player $playerName is within range!")
Image(/*some image*/)
Button(onClick = { attack() }) {
Text(text = "ELIMINATE") }
}else {
Text("No players nearby. Keep searching.")
Image(/*some OTHER image*/)
Button(onClick = { attack() }) { //This button should be DISABLED
Text(text = "ELIMINATE")
}
}
I am trying to add multiple GeoJsonLayer. When user click on 1 polygon I would like to display data of clicked polygon. Those are some of my polygons:
This is my function in which I get data from API and draw geojson layers on google maps.
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
var geojson = ArrayList<GeojsonResponse>()
val userGerkId: String? = SharedPrefManager.getInstance(applicationContext).user.gerkMID
RetrofitClientLands.instance.getLand(userGerkId).enqueue(object : Callback<GeojsonResponse> {
override fun onResponse(
call: Call<GeojsonResponse>,
response: Response<GeojsonResponse>
) {
if (response.code() == 200) {
val body = response.body()
if (body != null) {
for (i in 0 until body.lands.size) {
val geo = body.lands[i]
val geos = geo.get("geometry")
val properties = geo.get("properties")
//Log.i("Properties", properties.toString())
val geometryJson: JSONObject = JSONObject(geos.toString())
val geoJsonData: JSONObject = geometryJson
val layer = GeoJsonLayer(mMap, geoJsonData)
val style: GeoJsonPolygonStyle = layer.defaultPolygonStyle
style.fillColor = resources.getColor(R.color.darkGray)
style.strokeColor = resources.getColor(R.color.darkerGray)
style.strokeWidth = 2f
layer.addLayerToMap()
layer.setOnFeatureClickListener(
GeoJsonOnFeatureClickListener { feature: Feature ->
Toast.makeText(
applicationContext,
"GeoJSON polygon clicked: $properties",
Toast.LENGTH_SHORT
).show()
})
}
} else {
Log.i("Map-error", response.errorBody().toString())
}
}
}
override fun onFailure(call: Call<GeojsonResponse>, t: Throwable) {
Log.i("Map response", t.message.toString())
Toast.makeText(
applicationContext,
"Prišlo je do napake, na novo zaženite aplikacijo",
Toast.LENGTH_LONG
).show()
}
})
// adding marker
mMap.moveCamera(CameraUpdateFactory.newLatLng(LatLng(45.92757404830929, 15.595209429220395)))
mMap.uiSettings.isZoomControlsEnabled = true
mMap.animateCamera( CameraUpdateFactory.zoomTo( 12.5f ) );
}
I tried to set style.isClickable = false and then add code below, but every time i clicked on layer, it returns the same data (because whole map is clickable ig).
mMap.setOnMapClickListener {
Log.i("Map_clicked", "polygon: $properties")
}
So is there any other way of doing this? This thread has the same problem described.
How to add multiple GeoJsonLayer runtime and get click event in android
I'm working on a trading app. When the user select some products I need to show for each product if the market is open and its latest price. The user can select for example 2 products and what I have to do is to show the data only when I have all the info for the 2 products. The data can change at any time (i.e. the market for one of the products got closed). This is my code:
data class ProductInfo(
val productCode: String,
val isMarketOpen: Boolean,
val latestPrice: BigDecimal,
)
// This observable could emit at any time due to user interaction with the UI
private fun productsCodeObservable(): Observable<List<String>> = Observable.just(listOf("ProductA", "ProductB"))
// Markets have different working hours
private fun isMarketOpenObservable(productCode: String): Observable<Boolean> {
return Observable.interval(2, TimeUnit.SECONDS)
.map {
// TODO: Use API to determine if the market is open for productCode
it.toInt() % 2 == 0
}
}
// The product price fluctuates so this emits every X seconds
private fun latestPriceObservable(productCode: String): Observable<BigDecimal> {
return Observable.interval(2, TimeUnit.SECONDS)
.map { productPrice -> productPrice.toBigDecimal() }
}
#Test
fun `test`() {
val countDownLatch = CountDownLatch(1)
productsCodeObservable()
.switchMap { Observable.fromIterable(it) }
.flatMap { productCode ->
Observable.combineLatest(
isMarketOpenObservable(productCode),
latestPriceObservable(productCode)
) { isMarketOpen, latestPrice ->
ProductInfo(productCode, isMarketOpen, latestPrice)
}
}
.toList()
.doOnSuccess { productsInfo ->
println(productsInfo)
}
.subscribe()
countDownLatch.await()
}
I don't know what the problem is because the test method never prints anything. I don't know much about RxJava but my understanding is that toList is not working because the source observables never complete. Any idea about how I can collect the data for the product codes and emit a list when any of the data changes? :)
If you want to receive new product info list every time any of these products has changed:
productsCodeObservable()
.switchMap { list ->
val productInfoObservables = list.map { productCode ->
Observable.combineLatest(
isMarketOpenObservable(productCode),
latestPriceObservable(productCode)
) { isMarketOpen, latestPrice ->
ProductInfo(productCode, isMarketOpen, latestPrice)
}
}
Observable.combineLatest(productInfoObservables) { it.toList() as List<ProductInfo> }
}
.doOnNext { productsInfoList ->
println(productsInfoList)
}
.subscribe()
Use the RxJava’s Emitter interface and implement its methods:
public interface Emitter<T> {
void onNext(T value);
void onError(Throwable error);
void onComplete();
}
I think you need an ObservableEmitter, Please take a look at the following page:
https://www.raywenderlich.com/2071847-reactive-programming-with-rxandroid-in-kotlin-an-introduction
I ran into a problem that onNext cannot contain return, but I need to return the string.
The request is made using Retrofit with a factory inside the interface (ApiService).
fun getNameAnimal(name : String) : String {
var nameAnimal = " "
val api = ApiService.create()
api.getAnimal("Cat")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ animal ->
// It works
Log.i(LOG, animal.name)
// It NOT works (empty value)
nameAnimal = animal.name
},
{ error ->
Log.e(LOG, error.printStackTrace())
}
)
return nameAnimal
}
In the logs, the answer comes in the format I need.
The method is in a class that is not an activity or fragment.
How can I implement my plan?
fun getNameAnimal(name : String) : Single<String> {
val api = ApiService.create()
return api.getAnimal("Cat")
.map { animal -> animal.name }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
2. In activity or fragment
apiWorkingClassInstance.getNameAnimal()
.subscribe(
{ animalName ->
Log.i(LOG, animalName)
//todo
},
{ error ->
Log.e(LOG, error.printStackTrace())
}
)
Thanks for the tip Alex_Skvortsov
An easy and clean way to achieve this would be to use an interface.
Create your interface
interface AnimalCallbacks {
fun onAnimalNameReturned(name: String)
}
Have your class implement the interface
class MyAnimal: AnimalCallbacks {
override fun onAnimalNameReturned(name: String) {
//handle logic here
}
}
From here you can pass your interface in the method call and send the result back using the method you defined in the interface.
fun getNameAnimal(name : String, callbacks: AnimalCallbacks) {
var nameAnimal = " "
val api = ApiService.create()
api.getAnimal("Cat")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ animal ->
// It works
Log.i(LOG, animal.name)
// It NOT works (empty value)
nameAnimal = animal.name
callbacks.onAnimalNameReturned(animal.name)
},
{ error ->
Log.e(LOG, error.printStackTrace())
}
)
}
I am working with the java rx library now, I am having problems with the internal calls with flatMap and map, for some reason the most internal call is not been done.
Besides, if I replace the flatMap with suscribe() in the internal call, (before the childrenItemsResponse.forEach code), this code is executed, bat the execution is not synchronous, I mean this call is done after the main flatMap execution finish.
This is my code:
override fun getSportList(dCSServiceContext: DCSServiceContext): Single<List<EntityBrowse>> {
return dCSService.get(dCSServiceContext).flatMap { item ->
val entityBrowseList = arrayListOf<EntityBrowse>()
val section = builSection(item?.firstOrNull()!!)
if (item.firstOrNull()?.collections?.get("NavItems")?.size!! > 0) {
dCSServiceContext.contentIds = item.firstOrNull()?.collections?.get("NavItems")
buildNavItems(dCSServiceContext).map { section ->
return#map section
}.map { items ->
section.items = items
return#map entityBrowseList
}
} else {
Single.just(entityBrowseList)
}
}
}
The problem is presented in the buildNavItems, method:
private fun buildNavItems(dCSServiceContext: DCSServiceContext): Single<MutableList<Item>> {
return dCSService.get(dCSServiceContext).map { itemsResponse ->
val items: MutableList<Item> = arrayListOf()
itemsResponse.forEach { item ->
val transformedItem = buildItem(item!!)
if (item?.collections?.get("NavItems") != null) {
dCSServiceContext.contentIds = item?.collections?.get("NavItems")
val childrenItems: MutableList<Item> = arrayListOf()
dCSService.get(dCSServiceContext).flatMap { childrenItemsResponse ->
childrenItemsResponse.forEach { childrenItem ->
val transformedChildrenItem = buildItem(childrenItem!!)
childrenItems.add(transformedChildrenItem)
}
val section = Section("", "", false,childrenItems )
val data = Data(section)
val children = Children(data)
transformedItem.children = children
items.add(transformedItem)
Single.just(items)
}
} else {
val transformedItem = buildItem(item!!)
items.add(transformedItem)
}
}
return#map items
//Single.just(items)
}
}
More specifically, the in the line code: dCSService.get(dCSServiceContext).flatMap { childrenItemsResponse ->
This code is never executed.
I am not sure about what could be the cause of the issue.
Thanks in advance!!
To activate an observer chain, there has to be a subscription. You don't subscribe to the observer chain that starts with dCSService.... flatMap() itself won't subscribe to its interior observables until the outer observable chain has been subscribed to.