在上一篇Xamarin开发环境及开发框架初探中,曾简单提到MvvmCross这个Xamarin下的开发框架。最近又评估了一些别的,发现老牌Mvvm框架Prism现在也支持Xamarin Forms了,可喜可贺!今天我们就来近距离尝试、比较一下,分别基于这两个框架写一个简单Android/iOS跨平台应用的感受。
Prism
Prism框架应该来源于微软的Microsoft patterns & practices的Prism Guidance。最初,应该被更多用于WPF开发。我对WPF了解太少,就不评论它的过去了,我们这里重点关注他目前Preview状态的Prism Xamarin Forms支持。它提供了一个VS2015的模版插件,可以从这里直接下载安装,也可以从VS2015的Extensions and Updates菜单里面下载安装。安装这个插件,并重启VS2015后,我们可以看到,项目模版里多了一个Prism分组,包含三个项目类型,其中一个是Prism Unity App (Forms),就是for Xamarin Forms的。名字里包含Unity,那是指这个模版使用Unity DI框架。
事实上Prism支持Autofac,NInject等各种DI框架,不过模版只有Unity的。我们来尝试新建一个项目。相比于Xamarin官方及其他开发框架,它的项目模版,提供了非常友好的UI,可以选择新建的项目需要支持哪些平台。如下图:
我们就选Android和iOS。新建的项目很自然的包含一个PCL格式的共享项目,和一个Droid项目,一个iOS项目:
从目录结构来看,和Xamarin Forms官方的模版生成的项目非常类似,表面看来,好像就是在共享项目里多了ViewModels和Views目录。我们来看看Droid的MainActivity类,我们知道这个类是每一个Xamarin Droid项目的入口类,基本相当于Console程序的Main方法的地位。可以看到,LoadApplication()方法还是接受一个App类的instance,(这个类还是定义在共享项目里的,我们下面再细说),不过,这个App类的构造函数接受一个实现了Prism的IPlatformInitializer接口的AndroidInitializer。它只包含一个RegisterTypes(UUnityContainer container)方法,非常容易理解,就是用来配置Unity容器的,我们可以在这里注册各种业务层、数据层等等的接口和类到Unity。
好像Droid项目相比官方Xamarin Forms模版也就这点区别了,可见Prism对官方模版架构的改动非常小。我们再来看看iOS项目的AppDelegate类。这个类和MainActivity的地位相当,也是iOS项目的入口类。看上去也是和MainActivity做的事情非常类似,这里也包含一个iOSInitializer类实现了IPlatformInitializer接口。太容易理解了,自描述,都不需要文档,我喜欢。
下面就剩下在共享项目了。我们先来看看App类,它还是包含一个App.xaml和一个code behind文件。App.xaml看上去和Xamarin Forms官方模版的不一样了,它继承于PrismApplication类。
App.xaml.cs的内容就有比较大的不同了。首先,他当然需要接受一个IPlatformInitializer,并传递给基类,这个容易理解。此外,两个override方法,一个RegisterTypes()用于注册功能页面。另一个OnInitialized()用于指定默认打开那个页面,并且还可以给页面传参数。还是足够简单的。
很明显,项目运行时,会先显示MainPage,而MainPage的xaml和对应的ViewModel分别定义在Views目录和ViewModels目录中。MainPage是一个标准的xaml page,唯一的和Xamarin Forms版本不同的是,它包含了一个prism:ViewModelLocator.AutowireViewModel="True"的属性,从字面意思,我们就能知道,它表示,他会自动resolve并绑定到一个同名ViewModel,也就是ViewModels目录中的MainPageViewModel类。而这个类的实现也是不能再简单了。主要就一个构造函数加两个方法,一个OnNavigatedFrom(),一个OnNavigatedTo(),真的不需要文档,就能理解,一个是页面load时执行的逻辑,一个是页面离开时执行的逻辑。
让我们来尝试增加一个第二个页面和对应的ViewModel,看看页面跳转怎么玩。我们来新建一个SecondPage.xaml和对应的SecondPageViewModel类。页面功能就照抄MainPage了,只是显示的text不同。然后,我们需要在App类的RegisterTypes()里注册SecondPage:
接下来,我们来给MainPage.xaml增加一个按钮,点击切换到SecondPage:
当然,MainPageViewModel里我们也要添加相应的代码,主要是,要依赖注入一个INavigationService,用来执行跳转,当然还要一个LoadSecondCommand绑定到前面那个按钮。
Ok, 运行程序,点击按钮,就能从MainPage跳转到SecondPage了。
Prism的简单示例就到这儿。总体的感觉就是,Prism框架和Xamarin Forms本身的架构结合地的非常自然,接口定义,和编码方式也非常符合一般人的思路,几乎不需文档就能理解。
MvvmCross
下面我们再来近距离看看MvvmCross的情况。MvvmCross也有一个VS2015的插件,提供了支持Forms的项目模版。我们来练一把。下载安装这个插件后。我们来新建一个项目。新建的项目是下面这个样子:
两个Test项目我们先不管。我们可以看到,它相比Prism多了不少东西,我们一个一个来看。这次,我们先从共享项目开始。Model和Repository目录可以先忽略,他们只是生成了数据访问的repository接口和Model,其实和MvvmCross本身没什么关系。Resources和Services目录也可以先放一边,因为,它们实现了一个简易的多语言支持方案,接口其实非常简单明了。ViewModels和Views目录,其实和Prism的是一个概念,不过略有差别。
第一个差别是,MainPage.xaml中定义了一个xmlns:res="clr-namespace:MvvmCrossForms1.Core.Resources;assembly=MvvmCrossForms1.Core"属性,做什么用的呢?通过它可以访问Resources目录中定义的多语言翻译的文本资源。有了它,我们可以很方便的绑定多语言文本资源到xaml上的属性。
第二个差别是,所有的ViewModel类需要继承自MvvmCross提供的MvxViewModel类。并且,需要为每个ViewModel属性的set实现,调用基类的RaisePropertyChanged()方法。这样,理论上,它就能动态获知View上的属性变更,为实现动态数据绑定,提供了可能。
接下来,是App类。虽然名字还是叫App,实际上,它这个App类并不是最终继承自Xamarin Forms的Application类的,它不是一个View,也没有xaml,而是继承于MvvmCross自己的Application类。在具体的Driod和iOS项目到时可以看到,它的框架内部实际上封装了真正的Xamarin Application类。这个App类只包含一个Initialize()方法实现,前半部分调用它的内部IoC容器的配置方法,将所有的Services和Repositories接口和类,注册到了IoC容器。中间设置了App的当前语言。最后一行,调用RegisterAppStart<ViewModels.MainViewModel>()显示默认的MainPage页面。但是注意,比较特别的是,它不是通过页面的名称,而是通过ViewModel类,强类型方式制定要显示哪个页面的。
好了,共享项目就这些特别了。我们来演示如何进行页面跳转。新建名叫SecondPage的xaml和一个名叫SecondViewModel的ViewModel类。然后,给MainPage.xaml添加一个Button如下:
然后,在MainViewModel类中,添加一个ShowSecondCommand就行了,这个比Prism的版本简单多了。注意,它这里也是通过强类型的ViewModel进行页面跳转的。强类型的好处是,如果有任何重构,所有的引用会自动更新,不像Prism的字符串,还要自己手动改。
再来看Droid项目。Droid项目的Bootstrap目录一般用于MvvmCross的各种插件的初始化,这里不详述。Services目录呢,一般用于放置共享项目中定义的某些功能接口的Droid特定实现,这里也可以先忽略。Linker类可以忽略,是它生成了给MvvmCross框架自己用的。
这个Droid项目真正的入口是SplashScreen,它基于标准的Xamarin Activity提供了一个App开机页面的默认实现。它的isInitializationComplete()方法,指定了,App完成后,显示MvxFormsApplicationActivity这个Activity。
SplashScreen的基类内部,会通过反射,去寻找项目中一个名叫Setup的类,这个Setup类里,就是放所有App初始化代码的地方。所有的初始化执行完之后,SplashScreen类的isInitializationComplete()方法才会被调用。
一旦isInitializationComplete()方法被调用,MvxFormsApplicationActivity类被执行,在它的OnCreate()方法会最终启动,定义在共享项目里的App类,然后MainPage才会被真正显示。
iOS项目的情况,略有不同,没看到类似Droid项目的SplashPage功能(可能是不同的team负责iOS和Droid部分开发的?貌似Droid版本功能更完善一些。这个还要在摸索下)。iOS项目的启动类还是AppDelegate,它的FinishedLaunching()方法简单的调用了Setup类,并且直接启动了共享代码里的App类,这样MainPage就能被显示了。
老实说,相比Prism的执行过程,MvvmCross的确实有点绕。不过,它帮你做了好几件任何App开发可能都需要处理的事,包括开机画面、初始化管理、简化的依赖注入、多语言支持,更简单的页面跳转,更简单的数据绑定,另外它还提供了很多其他的插件模块。最关键的是,在MvvmCross的架构下,那些真正复杂难懂的部分,其实都是框架级别的代码,而增加View,ViewModel,Service等这些业务相关的模块的管理,反而比Prism下更简单。所以,如果要我选的话,如果要开发的是一个企业App,功能繁多,需要支持多语言的App,我可能还是会倾向于MvvmCross。我也喜欢Prism的优雅,简洁。真心希望Prism未来能提供更多方便使用的插件,当然,也许只是我孤陋寡闻,果真如此的话就求之不得了!
BTW,不知道为什么,在我本机的iOS模拟器中,升级到iOS 10以后,MvvmCross的iOSApp运行都会闪退,感觉是MvvmCross的bug,希望能尽快有人fix。Prism的iOS项目运行没有任何问题。如果谁知道原因,也请不吝赐教,谢谢!
Update 1:
在MvvmCross的issue tracker新建了一个issue,描述了这个crash的问题:https://github.com/MvvmCross/MvvmCross/issues/1452
Update 2:
上面提到的iOS debugging MvvmCross app crash的问题已经解决了,我删了模拟器里所有以前部署的app,再次debug就可以运行了,很神奇。怀疑是不是,以前的某个app部署过程出错了,然后,后续部署的app和以前有问题的这个app冲突了。不过,至少咱有活路了不是?
Update 3:
免费附赠两行脚本,当Xamarin iOS app调试出错时,如何查看iOS模拟器的device log呢?只需要在MacOS里起一个terminal,执行下面的两条命令:
# list all device's device codes
instruments -s devices
# view latest log of a specific device
tail -f ~/Library/Logs/CoreSimulator/<DEVICE_CODE>/system.log