https://androidweekly.net/issues/issue-416
Designing and Working with Single View States on Android
https://zsmb.co/designing-and-working-with-single-view-states-on-android/
ViewState是用于标识View状态的对象,通过ViewState,可以将View的显示逻辑与控制逻辑抽离。在MVI、MVVM等设计模式中都可以看到它的身影。
用例:加载联系人信息
这个页面有三个状态:Loading、Loaded、Error。
共用data类
- 优点:类数目最少
- 缺点:会出现诸如
errored=true, loading=true
的异常状态
1 2 3 4 5 6
| data class ProfileViewState( val errored: Boolean = false, val loading: Boolean = false, val name: String? = null, val email: String? = null )
|
Sealed classes(密封类)
1 2 3 4 5 6 7 8 9 10
| sealed class ProfileViewState
object Loading : ProfileViewState()
object Error : ProfileViewState()
data class ProfileLoaded( val name: String, val email: String ) : ProfileViewState()
|
View
类里面的绘制方法如下,注意exhaustive
扩展函数能够在我们的when
语句漏写某个分支时,在编译过程就抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private fun render(viewState: ProfileViewState) { when (viewState) { Loading -> { viewFlipper.displayedChild = Flipper.LOADING } Error -> { viewFlipper.displayedChild = Flipper.ERROR } is ProfileLoaded -> { viewFlipper.displayedChild = Flipper.CONTENT profileNameText.text = viewState.name profileEmailText.text = viewState.email } }.exhaustive }
|
ViewFlipper
可以用来展示多个Children中的一个:
1 2 3 4 5 6 7 8 9
| class ProfileFragment : Fragment() { private object Flipper { const val LOADING = 0 const val CONTENT = 1 const val ERROR = 2 }
}
|
布局文件:
1 2 3 4 5 6 7 8 9 10
| <ViewFlipper android:id="@+id/viewFlipper" android:layout_width="match_parent" android:layout_height="match_parent">
<include layout="@layout/profile_loading" /> <include layout="@layout/profile_content" /> <include layout="@layout/profile_error" />
</ViewFlipper>
|
共享数据,独立状态
如列表下滑加载更多,已经获取到的item仍然显示在界面上。
使用data class
来做ViewState。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| data class ListViewState( val isLoading: Boolean, val items: List<String> )
private fun render(viewState: ListViewState) { progressBar.isVisible = viewState.isLoading wordAdapter.submitList(viewState.items) }
sealed class ListViewState
object Error: ListViewState()
data class ListReady( val isLoading: Boolean, val items: List<String> ): ListViewState()
|
更加复杂的UI
对于包含更多元素的UI,在when
语句之前要将共用逻辑抽出,以简化when
代码块。
复杂写法,每个分支里处理所有UI元素,容易遗漏,维护困难:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| private fun render(viewState: UploadViewState) { when (viewState) { Initial -> { uploadProgressText.isVisible = false progressBar.isVisible = false uploadDoneIcon.isVisible = false uploadStatusText.isVisible = false retryUploadButton.isVisible = false } is UploadInProgress -> { uploadProgressText.isVisible = true progressBar.isVisible = true uploadDoneIcon.isVisible = false uploadStatusText.isVisible = false retryUploadButton.isVisible = false progressBar.setProgressWithAnimation(viewState.percentage.toFloat()) uploadProgressText.text = "${viewState.percentage}%" } UploadFailed -> { uploadProgressText.isVisible = false progressBar.isVisible = false uploadDoneIcon.isVisible = false uploadStatusText.isVisible = true uploadStatusText.text = "Sorry, something went wrong." retryUploadButton.isVisible = true } UploadSuccess -> { uploadProgressText.isVisible = false progressBar.isVisible = false uploadDoneIcon.isVisible = true uploadStatusText.isVisible = true uploadStatusText.text = "Upload complete!" retryUploadButton.isVisible = false } }.exhaustive }
|
简化写法,提取各个状态专有UI元素,优先处理;when
语句只处理共享UI元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private fun render(viewState: UploadViewState) { uploadProgressText.isVisible = viewState is UploadInProgress progressBar.isVisible = viewState is UploadInProgress retryUploadButton.isVisible = viewState is UploadFailed uploadDoneIcon.isVisible = viewState is UploadSuccess uploadStatusText.isVisible = viewState is UploadFailed || viewState is UploadSuccess
when (viewState) { Initial -> { } is UploadInProgress -> { progressBar.setProgressWithAnimation(viewState.percentage.toFloat()) uploadProgressText.text = "${viewState.percentage.toInt()}%" } UploadFailed -> { uploadStatusText.text = "Sorry, something went wrong." } UploadSuccess -> { uploadStatusText.text = "Upload complete!" } }.exhaustive }
|
使用 ConstraintLayout Groups 简化
一个Group
可以同时控制多个View的可见性。
1 2 3 4 5
| <androidx.constraintlayout.widget.Group android:id="@+id/errorViews" android:layout_width="wrap_content" android:layout_height="wrap_content" app:constraint_referenced_ids="errorTitle, errorIcon, errorMessage" />
|
Easy Android Scopes
https://ryanharter.com/blog/2020/03/easy-android-scopes/
介绍Activity、Fragment发生Configuration Changed事件时的数据保存方法,类似AndroidX中的ViewModel。
作者实现了自己的一套Scoped Delegate
方案。
Jetpack ViewModel and string resources
https://www.rockandnull.com/android-viewmodel-resources/
在使用ViewModel时,作者试图将ViewModel实现与Android解耦,以便ViewModel可以无缝迁移到其它平台(如iOS、Web)。文中提供了两种思路。
方案一:使用ResourceId
将资源id作为LiveData进行发送,而非字符串本身。
1 2 3 4 5 6 7 8 9 10
| val errorMessageLiveData = MutableLiveData<Int>() [...] errorMessageLiveData.value = R.string.error_message_1
errorMessageLiveData.observe(this, Observer { Toast.makeText(context, getString(it), Toast.LENGTH_LONG).show() } )
|
方案二:使用资源工厂类(Resources Helper)
该类用于抽象不同平台的资源获取方式。
1 2 3 4 5
| class ResourcesHelper(private val applicationContext: Context) {
val errorMessage get() = applicationContext.getString(R.string.error_message) }
|
将该类注入(inject)到ViewModel
中,可以使其独立于不同平台实现,并且能够进行mock/fake。
在ViewModel中使用Context
使用AndroidViewModel
代替ViewModel
。
1 2 3 4
| class MyViewModel(private val application: Application) : AndroidViewModel(application) { val errorMessage get() = application.resources.getString(R.string.some_string) }
|
这种写法有一个问题是,用户切换语言无法生效。以及在测试时必须人工提供一个Context对象。
##
最后更新时间:
本文系作者原创,如转载请注明出处。欢迎留言讨论,或通过邮件进行沟通~