android 单向认证_Android中的另一种单向状态流架构

android 单向认证

There are many architectures for developing android applications like MVP, MVVM, MVI, etc. By architecture I am referring to presentation layer architecture, you may architect your application using Clean architecture, Hexagonal architecture, Onion architecture, etc. One of the architectures that have proven to be effective is unidirectional state flow which is highly embraced in the web world with React and Redux. In this story, I am going to talk about USF (Unidirectional State Flow) architecture which is inspired by Kaushisk’s talk and his sample app.

有许多用于开发android应用程序的体系结构,例如MVP,MVVM,MVI等。按体系结构,我指的是表示层体系结构,您可以使用Clean体系结构,Hexagonal体系结构,Onion体系结构等来构建应用程序。事实证明,单向状态流是有效的,React和Redux在Web世界中高度接受。 在这个故事中,我将讨论由Kaushisk的演讲及其示例应用程序启发而来的USF(单向状态流)体系结构。

This story is not about an introduction to USF. There are many great articles and videos on that topic. I will try to link the references at the end of the story. This story is about my opinion in USF and various problem that I have come across and my solutions to those problems. Hope it helps anyone trying to implement USF.

这个故事不是关于USF的介绍。 关于该主题有很多很棒的文章和视频。 我将尝试在故事结尾处链接参考。 这个故事是关于我在南佛罗里达大学的观点以及我遇到的各种问题以及针对这些问题的解决方案。 希望它能帮助任何尝试实施USF的人。

TL DR; I highly recommend going through the code to understand the concepts.

TL DR; 我强烈建议您仔细阅读代码以了解概念。

Recap

回顾

Just to recap on USF so that we are on the same page. Here is a visualization of USF.

回顾一下USF,以便我们在同一页面上。 这是USF的可视化。

Image for post
  1. Events: View produces events which are normally generated from UI elements or from system services.

    事件: View产生通常由UI元素或系统服务生成的事件。

  2. Results: Events are converted to results generally by ViewModel. Results contains data that is produced from the events.

    结果:事件通常由ViewModel转换为结果。 结果包含事件产生的数据。

  3. State: The results are converted to state that needs to be rendered by the view.

    状态:将结果转换为视图需要呈现的状态。

  4. Effects: These are one shot events like showing a toast or navigating to other screens.

    效果:这些是一次拍摄事件,例如显示敬酒或导航到其他屏幕。

Kaushik’s (One of the host of Fragmented podcast) implementation of USF is great. I highly recommend listening to episodes 148 and 151 to get better idea on USF.

Kaushik( 片段化播客的主持人之一)对USF的实现很棒。 我强烈建议听第148151集,以更好地了解USF。

But there are some issues that I found with his approach. I am not saying his approach is wrong or I am better. It’s just that I want to point out some things that I think in my opinion improves the overall architecture.

但是我发现他的方法存在一些问题。 我并不是说他的方法是错误的,否则我会更好。 我只是想指出一些我认为可以改善整体架构的内容。

View Update

查看更新

This is one of the major issue with this implementation. This is the ViewState of the app from kaushik’s movies example

这是此实现的主要问题之一。 这是kaushik的电影示例中应用程序的ViewState

data class MSMovieViewState(
    val searchBoxText: String? = null,
    val searchedMovieTitle: String = "",
    val searchedMovieRating: String = "",
    val searchedMoviePoster: String = "",
    val searchedMovieReference: MSMovie? = null,
    val adapterList: List<MSMovie> = emptyList()
)

This is the function that will render the view state.

这是将呈现视图状态的函数。

private fun render(vs: MSMovieViewState) {
        vs.searchBoxText?.let {
            ms_mainScreen_searchText.setText(it)
        }
        ms_mainScreen_title.text = vs.searchedMovieTitle
        ms_mainScreen_rating.text = vs.searchedMovieRating


        vs.searchedMoviePoster
            .takeIf { it.isNotBlank() }
            ?.let {
                Picasso.get()
                    .load(vs.searchedMoviePoster)
                    .placeholder(spinner)
                    .into(ms_mainScreen_poster)


                ms_mainScreen_poster.setTag(R.id.TAG_MOVIE_DATA, vs.searchedMovieReference)
            }
            ?: run { ms_mainScreen_poster.setImageResource(0) }


        listAdapter.submitList(vs.adapterList)
    }

One of the issue with this implementation is that the entire view is re-rendered whenever any of the attributes in the ViewState changes. For example, if we change the title then other attributes like adapterList , searchBox etc will be re-rendered.

此实现的问题之一是,只要ViewState中的任何属性发生更改,整个视图就会重新呈现。 例如,如果我们改变标题,然后其他属性一样adapterListsearchBox等将被重新绘制。

One of the solution that kaushik pointed out was to use some sort of diffing like DiffUtilCallback for lists and checking for value for string. But in my opinion this solution is not so scalable.

kaushik指出的解决方案之一是对列表使用DiffUtilCallback之类的差异 ,并为字符串检查值。 但是我认为这种解决方案不是那么可扩展。

The way that I have tried to solve this issue is by using a wrapper class ViewBox . Here is the implementation of the class.

我尝试解决此问题的方法是使用包装器类ViewBox 。 这是该类的实现。

class ViewBox<T>(
    val value: T,
    private val isChanged: Boolean = true
) {


    fun stateCopy(
        value: T = this.value,
        isChanged: Boolean = false
    ): ViewBox<T> {
        return ViewBox(value, isChanged)
    }


    fun resetCopy(value: T = this.value): ViewBox<T> {
        return ViewBox(value)
    }


    fun getValueIfChanged(): T? {
        return if (isChanged) {
            value
        } else {
            null
        }
    }


    override fun toString(): String {
        return "ViewBox(value=$value, isChanged=$isChanged)"
    }


}

From now on I will be referencing my own implementation of the moviesUSF. Here is the ViewState for my own implementation.

从现在开始,我将引用我自己对movieUSF的实现。 这是我自己的实现的ViewState。

data class HomeState(
    val searchResult: ViewBox<List<Movies>> = ViewBox(listOf()),
    val searchStatus: ViewBox<ContentStatus> = ViewBox(ContentStatus.LOADED),
    val history: ViewBox<List<Movies>> = ViewBox(listOf()),
    val searchAnimation: ViewBox<ViewVisibility> = ViewBox(ViewVisibility())
) : State {


    fun stateCopy(
        searchResult: ViewBox<List<Movies>> = this.searchResult.stateCopy(),
        searchStatus: ViewBox<ContentStatus> = this.searchStatus.stateCopy(),
        history: ViewBox<List<Movies>> = this.history.stateCopy(),
        searchAnimation: ViewBox<ViewVisibility> = this.searchAnimation.stateCopy()
    ) = HomeState(searchResult, searchStatus, history, searchAnimation)


    fun resetCopy(
        searchResult: ViewBox<List<Movies>> = this.searchResult.resetCopy(),
        searchStatus: ViewBox<ContentStatus> = this.searchStatus.resetCopy(),
        history: ViewBox<List<Movies>> = this.history.resetCopy(),
        searchAnimation: ViewBox<ViewVisibility> = this.searchAnimation.resetCopy()
    ) = HomeState(searchResult, searchStatus, history, searchAnimation)


}

As you can see I have wrapped all the attributes within the state in ViewBox. Also I have defined two functions, one is stateCopy() that will mark the attributes not in the function parameter to not-changed. The resetCopy() function will mark all the attributes as changed. Don’t worry about ViewVisibility class, I will explain that later.

如您所见,我已经将所有属性包装在ViewBox的状态内。 我还定义了两个函数,一个是stateCopy() ,它将把不在函数参数中的属性标记为not-changedresetCopy()函数会将所有属性标记为changed 。 不用担心ViewVisibility类,我将在后面解释。

We can now only render those attributes that changed their values instead of entire state of the view.

现在,我们只能呈现那些更改了其值的属性,而不是视图的整个状态。

override fun render(state: HomeState) {
        Timber.d("------ rendering state: $state")
        state.searchResult.getValueIfChanged()?.let {
            Timber.d("------ rendering search results: $it")
            adapter.submitList(it)
        }
    }

This essentially acts as a diffing mechanism.

这本质上是一种差异机制。

But how do we know when the attribute has changed. Well the answer is simple, when converting Results to state , we wrap the state attributes in the ViewBox like this.

但是,我们如何知道属性何时更改。 答案很简单,当将Results转换为state ,我们将state属性像这样包装在ViewBox

override fun resultToState(results: Observable<out HomeResults>): Observable<HomeState> {
        return results
            .scan(HomeState()) { vs, result ->
                when (result) {
                    is ScreenLoadResult -> {
                        if (result.isRestored) {
                            // change state when the fragment is restored
                            vs.resetCopy()
                        } else {
                            vs.resetCopy()
                        }
                    }


                    is SearchMovieResult -> {
                        vs.stateCopy(
                            searchResult = ViewBox(result.movies), searchAnimation = ViewBox(
                                ViewVisibility(true, isAnimated = true)
                            )
                        )
                    }


                    is SearchMovieStatusResult -> {
                        vs.stateCopy(
                            searchStatus = ViewBox(result.status)
                        )
                    }


                    is AddMovieToHistoryResult -> {
                        vs.stateCopy(history = ViewBox(result.history))
                    }


                    else -> {
                        vs
                    }
                }


            }
            .distinctUntilChanged()
    }

The stateCopy() takes care of marking other attributes changed state to false.

stateCopy()负责将其他已更改状态的属性标记为false。

Initial View State

初始视图状态

With kaushik’s implementation there is ScreenLoadEvent which is fired whenever the activity or fragment is resumed. But this implementation implicitly emits the initial state even before the ScreenLoadEvent.

在kaushik的实现中,有一个ScreenLoadEvent ,每当恢复活动或片段时都会触发该事件。 但是此实现甚至在ScreenLoadEvent之前隐式发出了初始状态。

viewState = result
                    .resultToViewState()
                    .doOnNext { Timber.d("----- vs $it") }
                    .replay(1)
                    .autoConnect(1) { disposable = it }

As you can see the last state is replayed whenever a view subscribes to the ViewState. But in my opinion it will be better if we can control the initial emissions. That means we want to emit initial state when the view provides ScreenLoadEvent. This can be achieved with some Rx magic.

如您所见,只要视图订阅了ViewState,就会重播上一个状态。 但我认为,如果我们能够控制初始排放量会更好。 这意味着我们希望在视图提供ScreenLoadEvent时发出初始状态。 这可以通过一些Rx魔术来实现。

viewState = resultToState(o)
                    .doOnNext {
                        Timber.d("------ vs: $it")
                    }
                    .replay(1)
                    .autoConnect(1) {
                        addDisposable(it)
                    }
                    .skip(1)

We are just skipping the initial state with skip operator, so we are explicitly providing the initial state to the view. This is very important if you want to provide different initial state based on some condition.

我们只是使用skip运算符跳过了初始状态,因此我们明确地向视图提供了初始状态。 如果您要基于某些条件提供不同的初始状态,这非常重要。

override fun resultToState(results: Observable<out HomeResults>): Observable<HomeState> {
        return results
            .scan(HomeState()) { vs, result ->
                when (result) {
                    is ScreenLoadResult -> {
                        if (result.isRestored) {
                            // change state when the fragment is restored
                            vs.resetCopy()
                        } else {
                            vs.resetCopy()
                        }
                    }
                }
            }
    }

We can check weather the fragment is restored from backstack or not and provide different initial state based on that.

我们可以检查片段是否从后堆栈还原的天气,并基于此提供不同的初始状态。

Access State in code

代码中的访问状态

There are times when we need to access the current state somewhere in the code. But with current implementation the state is available only within the scan block of resultToState function.

有时,我们需要在代码中的某个位置访问当前状态。 但是在当前实现中,状态仅在resultToState函数的scan块内resultToState

To fix that I defined a new variable mState that stores the state observable and use withLatestFrom operator to obtain the latest value of the state like this.

为了解决这个问题,我定义了一个新的变量mState ,该变量存储可观察的状态,并使用withLatestFrom运算符来获取状态的最新值,如下所示。

mState = resultToState(o)
                    .doOnNext {
                        Timber.d("------ vs: $it")
                    }
                    .replay(1)
                    .autoConnect(1) {
                        addDisposable(it)
                    }


                state = mState.skip(1)

Then define withLatestFrom extension function to reduce the boiler plate code.

然后withLatestFrom扩展函数withLatestFrom定义以减少样板代码。

inline fun <T, U, R> Observable<T>.withLatestFrom(
    other: ObservableSource<U>,
    crossinline combiner: (T, U) -> R
): Observable<R> = withLatestFrom(other, BiFunction<T, U, R> { t, u -> combiner.invoke(t, u) })

We can then use this function in any observable to get the latest state like this

然后,我们可以在任何可观察的状态下使用此功能,以获取最新状态,如下所示

private fun loadMovieDetails(): ObservableTransformer<LoadMovieDetailsEvent, LoadMovieDetailsResult> {
        return ObservableTransformer { observable ->
            observable
                .withLatestFrom(mState) { event, state ->
                    val movie = state.searchResult.value[event.position]
                    LoadMovieDetailsResult(movie)
                }
        }
    }

Animation Issue

动画问题

To handle the animations and view visibility, I used the ViewVisibility wrapper class to encapsulate both the visibility and animation. I could have also used Effects to trigger the animation but since most of the time visibility and animations are tightly coupled, I just put them in a single class.

为了处理动画和视图可见性,我使用了ViewVisibility包装器类来封装可见性和动画。 我也可以使用“ Effects来触发动画,但是由于大多数时候可见性和动画是紧密结合的,因此我将它们放在一个类中。

LCE State

LCE州

Kaushik’s implementation returns Results wrapped in LCE. But in my opinion this might create issue when you have more than one part in the screen that shows progress indicators. So I generally use LCE in the repository layer and instead use ContentStatus class to pass the status to the view.

Kaushik的实现返回以LCE包装的Results 。 但是我认为,当您在显示进度指示器的屏幕中有多个部分时,这可能会引起问题。 因此,我通常在存储库层中使用LCE,而使用ContentStatus类将状态传递给视图。

private fun searchMovie(): ObservableTransformer<SearchMovieEvent, out HomeResults> {
        return ObservableTransformer { observable ->
            observable
                .filter {
                    it.query.isNotEmpty() || it.query.isNotBlank()
                }
                .switchMap { e ->
                    repo.getMoviesFromServer(e.query)
                        .withLatestFrom(mState) { response, state ->
                            Pair(response, state)
                        }
                        .flatMap { combinedResult ->
                            when (val response = combinedResult.first) {
                                is Lce.Loading -> {
                                    Observable.just(
                                        SearchMovieStatusResult(ContentStatus.LOADING)
                                    )
                                }


                                is Lce.Content -> {
                                    if (response.packet.isEmpty()) {
                                        Observable.just(
                                            SearchMovieStatusResult(ContentStatus.EMPTY),
                                            SearchMovieResult(
                                                listOf()
                                            )
                                        )
                                    } else {
                                        Observable.just(
                                            SearchMovieStatusResult(ContentStatus.LOADED),
                                            SearchMovieResult(response.packet)
                                        )
                                    }
                                }


                                is Lce.Error -> {
                                    Observable.just(
                                        SearchMovieStatusResult(
                                            ContentStatus.error(
                                                response.throwable?.message
                                            )
                                        ),
                                        SearchMovieResult(listOf())
                                    )
                                }
                            }
                        }
                }
        }
    }

Generic

泛型

To reduce boiler plate code I define generic interfaces that the ViewModel and Fragment should implement.

为了减少样板代码,我定义了ViewModel和Fragment应该实现的通用接口。

interface Event
interface State
interface Result
interface Effect


interface Reducer<E: Event, R: Result, S: State, F: Effect> {


    val state: Observable<S>
    val effect: Observable<F>


    fun eventToResult(events: Observable<E>): Observable<out R>
    fun resultToState(results: Observable<out R>): Observable<S>
    fun resultToEffect(results: Observable<out R>): Observable<F>
}


interface UsfView<E: Event, S: State, F: Effect> {
    fun render(state: S)
    fun trigger(effect: F)
}

Testing

测验

USF makes testing view model very easy. In my opinion if you properly architecture your code you can even skip Expresso testing. Here is the test class for the sample project.

USF使测试视图模型非常容易。 我认为,如果您正确构建代码,甚至可以跳过Expresso测试。 这是示例项目的测试类。

class HomeViewModelTest {


    private lateinit var viewModel: HomeViewModel
    private lateinit var stateTester: TestObserver<HomeState>
    private lateinit var effectTester: TestObserver<HomeEffects>


    private lateinit var repo: MoviesRepository


    private var isRequestSuccess = true


    private val movies = listOf(
        Movies("221", "blade", "2019", "action", "image.png"),
        Movies("222", "blade", "2019", "action", "image.png"),
        Movies("223", "blade", "2019", "action", "image.png"),
        Movies("224", "blade", "2019", "action", "image.png")
    ).toMutableList()


    @Before
    fun setup() {
        // mock the api
        repo = mock(MoviesRepository::class.java).apply {
            `when`(getMoviesFromServer(anyString())).thenAnswer {
                if (isRequestSuccess) {
                    Observable.just(Lce.Loading(), Lce.Content(movies))
                } else {
                    Observable.just(Lce.Loading<List<Movies>>(), Lce.Error(Throwable()))
                }
            }
        }


        viewModel = HomeViewModel(repo)
        stateTester = viewModel.state.test()
        effectTester = viewModel.effect.test()


        viewModel.processEvent(ScreenLoadEvent(false))
        stateTester.assertValueCount(1)
    }


    @Test
    fun whenSearchMovie_AndMovieFound_ShowResults() {
        viewModel.processEvent(SearchMovieEvent("blade"))


        stateTester.assertValueCount(4)
        stateTester.assertValueAt(3) {
            assertThat(it.searchResult.value.size).isEqualTo(movies.size)
            true
        }
    }


    @Test
    fun whenSearchMovie_AndQueryEmpty_NoSearch() {
        viewModel.processEvent(SearchMovieEvent(""))


        stateTester.assertValueCount(1)
    }


    @Test
    fun whenSearchMovie_AndMovieEmpty_ShowEmpty() {
        movies.clear()
        viewModel.processEvent(SearchMovieEvent("blade"))


        stateTester.assertValueCount(4)
        stateTester.assertValueAt(3) {
            assertThat(it.searchStatus.value).isEqualTo(ContentStatus.EMPTY)
            assertThat(it.searchResult.value.isEmpty()).isTrue()
            true
        }
    }


    @Test
    fun whenSearchMovie_AndApiError_ShowError() {
        isRequestSuccess = false
        viewModel.processEvent(SearchMovieEvent("blade"))


        stateTester.assertValueCount(4)
        stateTester.assertValueAt(3) {
            assertThat(it.searchStatus.value.status).isEqualTo(DataStatus.ERROR)
            assertThat(it.searchResult.value.isEmpty()).isTrue()
            true
        }
    }


    @Test
    fun whenSearchMovieSuccess_AnimateSearchButton() {
        whenSearchMovie_AndMovieFound_ShowResults()


        stateTester.assertValueCount(4)
        stateTester.assertValueAt(3) {
            assertThat(it.searchAnimation.value.isAnimated).isTrue()
            true
        }
    }


    @Test
    fun whenAddToHistoryClick_UpdateHistory() {
        // first trigger some search
        whenSearchMovie_AndMovieFound_ShowResults()


        viewModel.processEvent(AddMovieToHistoryEvent(0))


        stateTester.assertValueCount(5)
        stateTester.assertValueAt(4) {
            assertThat(it.history.value.size).isEqualTo(1)
            true
        }
    }


    @Test
    fun whenAddToHistoryClick_AndMovieAlreadyAdded_NoHistoryUpdate() {
        // trigger a add to history
        whenAddToHistoryClick_UpdateHistory()


        viewModel.processEvent(AddMovieToHistoryEvent(0))


        stateTester.assertValueCount(5)
    }


    @Test
    fun whenMovieClick_LoadMovieDetails() {
        whenSearchMovie_AndMovieFound_ShowResults()
        viewModel.processEvent(LoadMovieDetailsEvent(0))


        effectTester.assertValueCount(5)
        effectTester.assertValueAt(4) {
            assertThat(it::class.java).isAssignableTo(NavigateToDetailsEffect::class.java)
            true
        }
    }






    // setup the default schedulers
    companion object {
        @ClassRule
        @JvmField
        val schedulers = RxImmediateSchedulerRule()
    }
}

As you can see we can test almost every aspect of the view without even touching the view. I think this is one of the important benefits of using USF.

如您所见,我们几乎可以在不触摸视图的情况下测试视图的每个方面。 我认为这是使用USF的重要好处之一。

That’s it for this story. USF is very vast topic and I recommend you to checkout my sample app and go through the code. I have skipped many implementation details in this post to make it simple. If you have any questions or suggestion feel free to ask in comments.

这个故事就是这样。 USF是一个非常广泛的主题,我建议您签出我的示例应用程序并检查代码。 为了简单起见,我跳过了许多实现细节。 如果您有任何问题或建议,请随时在评论中提问。

翻译自: https://medium.com/swlh/yet-another-uni-directional-state-flow-architecture-in-android-6957f5f3b37b

android 单向认证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值