页面与ViewModel(下)

本文探讨了UWP应用开发中页面与ViewModel交互的最佳实践,包括数据差异更新算法以优化用户体验,以及通过计数器管理复杂的加载状态。还讨论了图片源绑定时的异常处理方案。
原文: 页面与ViewModel(下)

在上一篇博客中,笔者分享了一些从页面整体的角度对页面与ViewModel的思考。在本文中笔者希望从相对细节的角度分享一些对页面与ViewModel的思考。

比如,当我们在更新View Model中的绑定数据时,应该怎样更新呢?简单的自然可以用新的数据实例直接替代旧的,但是这样容易造成UI界面闪烁。尤其是绑定数据是一个列表的情况下,如果整个列表被替换,可以非常明显的看到列表"一闪"。这样的用户体验无疑是不理想的。那么我们在更新View Model中绑定的数据实例时,可以采用差异更新的方法。以一个数据列表为例,在更新时对比新旧列表,先遍历新表,对每一个元素查看在旧表中有无对应元素。如果没有,说明是新增的数据,可以将该新表中的元素同时加入到一个临时表和旧表中,如果旧表有排序则还需要注意插入的位置。如果有,说明是旧元素更新,则用新元素的值更新旧元素后,将旧元素加入到临时表中。然后遍历旧表,对旧表中每一个元素在临时表中查看有无对应元素。如果有,则不用做任何处理。如果没有,则说明该元素已经被删除,应该在旧表中将这个元素移除。这样对UI界面的更新看起来会比较平滑。

这里写一下笔者在旺信UWP中所写的差异化更新算法,权当抛砖引玉。

            var bList = new List<bool>();//辅助列表
            for (int j = 0; j < MainList.Count; j++)//辅助列表初始与旧表同长
            {
                bList.Add(false);
            }
            for (int i = 0; i < groups.Count; i++)//遍历新表
            {
                bool inserted = false;
                bool contains = false;
                for (int j = 0; j < MainList.Count; j++)//新表中的元素与旧表对比
                {
                    if (groups[i].key != MainList[j].key)//如果不是同一元素
                    {
                        if ((groups[i].key == "群主")//尝试插入
                            || (
                                MainList[j].key != "群主" && MainList[j].key != "管理员"
                                && (groups[i].key == "管理员" || groups[i].key.CompareTo(MainList[j].key) < 0)
                                )
                            )
                        {
                            MainList.Insert(j, groups[i]);
                            bList.Insert(j, true);
                            inserted = true;
                            Debug.WriteLine("inserted:" + j + "," + groups[i].key);
                            break;
                        }
                    }
                    else//如果是同一元素,用新表元素内容更新旧表
                    {
                        contains = true;
                        MainList[j].update(groups[i]);
                        bList[j] = true;
                        break;
                    }
                }
                if ((!contains) && (!inserted))//不包括在旧表内,也没有插入,则追加在旧表尾部
                {
                    MainList.Add(groups[i]);
                    bList.Add(true);
                }
            }
            for (int i = bList.Count; i > 0; i--)//对比辅助列表,移除旧表中不应再存在的元素
            {
                if (!bList[i-1])
                {
                    try
                    {
                        MainList.RemoveAt(i - 1);
                    }
                    catch (Exception)
                    {
                        Debug.WriteLine("RemoveAt error:" + i);
                    }
                }
            }

在这段代码中,用新的数据groups更新旧的数据MainList

再比如,在我们的页面上,我们一般都会放置一个表示正在加载数据的控件。这个加载中控件的状态一般也是绑定一个后台数据的。对于一般的页面,我们可以采取在加载数据前后设置该绑定值的方法来修改页面所显示的加载状态。而对于UWP旺信这种依赖网络,一个页面可能同时调用多个网络接口更新数据的情况,就不是非常合适了。比如a,b两个接口同时请求数据,将加载状态置为加载中。如果a接口先返回,则会将加载状态置为完成。而实际上b接口仍然在请求数据,正确的加载状态应该还是加载中,直到b接口也返回。为此笔者想到了可以增加一个初始值为0的计数变量,当有加载请求时就自增1,当请求异步结束或回调返回时就自减1,绑定的加载状态的get方法根据当计数是否为0返回是否在加载状态。这样一来就可以使多个加载请求都能正确的改变加载状态。

在旺信UWP中,笔者就为ViewModel添加了这样的变量:

 

        public bool isLoading
        {
            get { return loadingCount > 0; }
        }

        private int _loadingCount = 0;

        public int loadingCount
        {
            get { return _loadingCount; }
            set { _loadingCount = (value < 0 ? 0 : value); RaisePropertyChanged("isLoading"); }
        }

在xaml页面上则将ProgressRing控件的IsActive属性绑定到isLoading变量上:

<ProgressRing Grid.Row="2" Grid.RowSpan="2" IsActive="{x:Bind thisData.isLoading,Mode=OneWay}" Width="60" Height="60" Foreground="{ThemeResource WXThemeColorBrush}"></ProgressRing>

在调用异步方法前后,并不直接设置isLoading变量,而是采取上面提到的,调用前loadingCount变量自增1,完成后loadingCount变量自减1的方法来影响ProgressRing控件所显示的加载状态IsActive。

另外,在使用x:Bind方法时,笔者发现如果把绑定image图片控件的source绑定到一个string,当绑定的string值为空时会在log中出现exception。即使在string值为空时把image控件隐藏也仍然会出现。然而旺信中数据的属性值基本都是从服务器传输到客户端的,有时确实会有一些图片的url为空。这样一来最好给图片属性值都给一个默认值。那么默认值该如何确定呢?如果是普通的占位图片,那么可能在不该出现图片的地方显示。经过实践,笔者选择了在应用中加入一个长度为0的图片,把该图片的uri作为图片属性的默认值。当然这个方法只是消除了log中的exception,具体是否提升了应用的效率,还有待验证。

以上就是笔者对对页面与ViewModel的细节的思考,希望能对小伙伴们开发UWP应用有所帮助。当然也欢迎大家拍砖,提出更多更好的经验,让我们共同进步。

posted on 2017-09-20 14:09 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/7561220.html

### ViewModel之间的通讯方法及最佳实践 在现代 Android 开发中,`ViewModel` 的设计初衷是为了管理 UI 层的状态并业务逻辑解耦。然而,在某些复杂场景下,可能需要多个 `ViewModel` 实现相互协作或共享状态。以下是常见的实现方式及其优缺点: #### 1. 使用 Shared ViewModel 当两个或多个 `Fragment` 需要共享同一个 `ViewModel` 时,可以通过它们共同的父级组件(通常是 `Activity` 或另一个容器 `Fragment`)来创建和持有该 `ViewModel`。 ```kotlin class SharedViewModel : ViewModel() { val sharedData = MutableLiveData<String>() } ``` 在子 `Fragment` 中获取这个 `SharedViewModel`: ```kotlin val sharedViewModel: SharedViewModel by activityViewModels() ``` 这种方式适用于父子关系中的 `Fragment` 和 `Activity` 间的通信[^2]。它简单易用,并且能够很好地利用 `ViewModel` 的生命周期特性。 --- #### 2. 利用事件总线 (Event Bus) 如果应用中有许多独立模块或者跨页面间存在复杂的交互需求,则可以考虑引入第三方库如 GreenRobot EventBus 来作为消息传递机制。 发送事件: ```java public class MessageEvent { /* Additional fields if needed */ } // 发送事件 bus.post(new MessageEvent()); ``` 接收事件: ```java @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(MessageEvent event) {/* Do something */}; ``` 尽管这种方法灵活性高,但在实际项目里容易造成代码难以维护的情况,因此需谨慎选用[^3]。 --- #### 3. 基于 Repository 模式的间接沟通 推荐的方式是让所有的 `ViewModel` 访问统一的数据仓库 (`Repository`) ,而不是直接互相调用彼此的方法。这样做的好处是可以保持各层职责清晰分离,同时也便于单元测试。 示例架构图如下所示: - **UI Layer**: 包含 Activities/Fragments 及其对应的 ViewModels; - **Domain Logic/Business Rules**: Encapsulated within Repositories; - **Data Sources**: Includes local databases, remote APIs etc. 通过这样的分层结构,即使某个特定功能涉及到了不同界面的操作,也可以只修改 repository 而不影响其他部分[^4]。 --- #### 4. 运用 LiveData/Flow 组合模式 借助 Kotlin 协程的支持,我们可以采用更现代化的方式来处理异步流式数据更新——即 Flow API 。相比于传统的回调函数或是 RxJava 流程控制手段来说,Flows 提供了一个更加简洁直观的选择方案。 下面是一个简单的例子展示如何在一个 viewmodel 内部定义 flow 并暴露给外部观察者订阅变化: ```kotlin private val _uiState = MutableStateFlow<UiState>(initialValue) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun fetchData(){ viewModelScope.launch{ try{ val result=repository.getDataAsync().await() _uiState.value=Success(result) }catch(e:Exception){ _uiState.value=Error(e.message?:"Unknown error occurred") } } } ``` 这里需要注意的是,由于 flows 默认运行在线程池上执行耗时操作,所以记得切换回主线程前完成所有必要的计算工作[^5]。 --- ### 总结 综上所述,针对 “ViewModel 如何进行通讯”这个问题的回答涵盖了四种主要途径:分别是基于活动范围内的共享视图模型;全局广播型的消息队列工具;经由中介服务对象来进行协调互动;最后还有结合协程特性的新型响应式编程范式。每种策略都有各自适用场合,请依据具体情况选取最合适的解决方案。 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值