使用QML + Javascript作为主要编码语言,与其他语言和框架相比,这可以节省高达90%的代码。QML的简单性加快了开发速度,可定制性和可扩展性使其变得如此强大。
您也不必担心让UI内的值保持最新。凭借属性绑定的强大功能,QML项目及其属性会自动更新。
虽然所有这些功能都非常有用,但QML的易用性和灵活性也会导致问题。在幕后,属性在发生变化时会发出信号。因此,依赖于现在更改的值的其他属性将处理此信号并更新其值。这也适用于多个QML项目:
现在看起来并不复杂,但想象一下,添加了一些具有不同属性和交叉依赖性的新组件:
所有项目之间的不同联系就这样形成了。您无法再确定单个属性更改对直接或间接连接的组件可能产生的影响。在最坏的情况下,这可能导致循环依赖或循环。如果您遇到此类问题,这可能是错误组件架构的一个指标。
项目增长越多,这个问题就越大越大。因此,每个组件应具有干净的界面,并且只管理自己的数据。这就是关注点的分离意味着什么。
每个应用程序的核心在于其数据。每个应用程序都会检索,处理,存储和显示某种特定于域和用例的信息。因此,以干净的方式管理和存储数据至关重要。
糟糕的组件架构很快就会导致不必要的副作用或数据损坏。想象一下许多信号触发和事件处理程序以未知顺序运行。您在应用程序不同部分的代码会更改看似随机或重复代码的数据。这是调试,维护或重构的噩梦。
因此,从UI代码中拆分与数据相关的任务是有意义的。为实现这一目标,许多应用程序都采用了基于MVC(模型 - 视图 - 控制器)或MVVM(模型 - 视图 - 视图模型)设计模式的体系结构。这些模式的实际实现可能有所不同,但它们都有一个主要原则:将显示数据(视图)的代码与读取或修改数据(模型)的代码分开。
您的QML应用程序的每个内容视图通常都是一个页面(Page)。的页面类型是一个视图控制器,它为用户提供和显示数据:
这DataModel是应用程序数据的中央存储。页面只能访问应用程序数据DataModel。它以对您的用例和视图有意义的方式管理您的数据。使用这种架构,模型和页面之间的数据流是双向的。这意味着,页面不仅可以显示模型数据,还可以直接写入模型。
QML允许使用JavaScript添加应用程序逻辑,因此您可以快速将内联Javascript代码添加到不同的页面。例如属性更改的处理程序:
onPropertyChanged:{
// ...进行计算,更新视图,使用数据模型,...
}
结果是在视图项之间分布了大量碎片代码。一旦您的应用程序变得更复杂,维护代码清理和可测试就变得更加困难。它还使得很难确定执行哪个操作的位置,并且跨页面的重复代码是不可避免的。
像React Native这样的现代应用程序框架使用不同的方法。例如,Facebook设计的Flux架构支持单向数据流。每个用户交互仅通过中央调度程序将操作传播到包含应用程序数据和业务逻辑的各个存储:
很难说复杂解决方案的开销是否值得付出努力。对于大多数移动应用程序项目,具有相同优势的更轻松的实现就足够了。以下部分显示了实现单向数据流的简便方法。
我们可以应用该原则来创建单向数据流并引入专用Logic组件,该组件将用户操作转发到DataModel:
该Logic组件代表您的业务逻辑API。它包含一组操作,页面为不同的用户交互激活。它将每个动作转发给DataModel它,然后依次处理它。每当数据发生变化时,模型会通知所有受影响的页面。
这是Logic组件的示例:
import QtQuick 2.0
Item {
//动作
signal fetchTodos()
signal storeTodo(var todo)
}
我们可以利用QML的内置信号机制来定义逻辑动作。然后页面可以调用logic.fetchTodos(),该页面触发信号并通知订阅的组件。
某些用户操作可能包括模型的不同数据集上的多个操作。例如,用户注册需要在模型中创建用户帐户,以及记录新用户。在您的页面中,您有一个如下按钮:
AppButton {
text: “Register”
onClicked:{
logic.createUser(user)
logic.setCurrentUser(user)
}
}
一个信号处理程序中的多个操作表明您正在处理一个独立的用例,这需要一个自己的动作信号:
AppButton {
text: “Register”
onClicked:{
logic.registerUserAndLogin(user)
}
}
最佳做法是将每个用户交互与单个逻辑操作相匹配。如果涉及某些计算(不依赖于数据模型内部),则可以提供业务逻辑功能。例如,要计算Logic中两个日期之间的经过时间:
import QtQuick 2.0
Item {
signal storeElapsedTime(int time)
function storeElapsedTimeBetween(date1,date2){
var elapsed = getElapsedTime(date1,date2)
storeElapsedTime(elapsed)
}
// ...
}
这有助于仅在页面中保留必要的视图逻辑。如果不同的页面使用逻辑功能,您还可以避免重复的代码。
该DataModel管理所有数据。它很可能也会持久存储数据,或与REST服务进行通信。要从中处理用户在Logic的操作,可以按以下方式添加Connections组件:
import QtQuick 2.0
Item {
//用于配置目标调度程序/逻辑
property alias dispatcher:logicConnection.target
//模型数据属性
readonly property alias todos:_.todos
//信号
signal fetchTodosFailed(var error)
signal storeTodoFailed(var todo,var error)
signal todoStored(int id)
//从dispatcher中监听动作
Connections{
id:logicConnection
// action 1 - fetchTodos
onFetchTodos:{
//从api加载
api.getTodos(
function(data){
_.todos = data
},
function(error){
fetchTodosFailed(error)
})
}
// action 2 - storeTodo
onStoreTodo:{
//使用api存储
api.addTodo(todo,
function(data){
//添加新项待办事项
._todos.unshift(data)
todosChanged()
todoStored(data.id)
},
function(error){
storeTodoFailed(todo,error)
})
}
}
// rest api用于数据访问
RestAPI {
id:api
}
//私有
Item {
id:_
property var todos:[]
}
}
此示例模型还在私有子项中定义其数据属性。这样的设置有助于确保模型的公共接口仅允许读取数据。无法直接对数据模型执行操作或从外部更改数据。数据模型完全控制如何处理和存储数据。
上面的代码也使用示例性RestAPI项目。然后,此项可以使用HttpRequest类型来实现与REST服务的通信。您还可以使用其他API解决方案来存储数据,例如使用Firebase插件。
由于DataModel自身决定要处理哪些逻辑操作,因此可以将域数据拆分为多个模型。使用灵活的信号方法,您甚至可以添加自己的组件以进行日志记录以跟踪用户操作:
每个模型都可以处理相关的操作,并提供其域的数据。通过订阅逻辑信号,Logging组件也被通知页面上的每个与数据相关的用户动作。
要使用页面中的Logic和DataModel类型,只需将它们添加到您的页面中Main.qml,然后将模型连接到逻辑:
导入QtQuick 2.0
App {
// model
DataModel {
id: dataModel
dispatcher: logic // data model处理逻辑发送的动作
//错误处理
onFetchTodosFailed:nativeUtils.displayMessageBox(“无法加载待办事项”,错误,1)
onStoreDraftTodoFailed:nativeUtils.displayMessageBox(“无法存储” + todo。标题)
}
//业务逻辑
Logic {
id:logic
}
//View
NavigationStack {
initialPage:TodoListPage {}
}
// app initialization
Component .onCompleted:{
// fetch todo list data
logic.fetchTodos()
}
}
您可以使用App的Component.onCompleted处理程序进行初始化,例如,获取所需的数据。
该TodoListPage示例显示了所有待办事项,并允许添加新条目:
import QtQuick 2.0
ListPage {
id: page
title: qsTr( “Todos”)
//添加新的todo
rightBarItem: IconButtonBarItem {
icon:IconType.plus
onClicked:{
var todo = {
completed:false,
title:“新Todo”,
userId:1
}
logic.storeTodo(todo)
}
}
//显示数据模型
model:dataModel.todos
delegate:SimpleRow {
text:modelData.totle
}
}
当用户按下按钮添加待办事项时,页面仅发送logic.storeTodo(todo)。在DataModel得到通知,存储项目并更新dataModel.todos属性。属性绑定model: dataModel.todos也替换了ListPage模型。因此,新的待办事项项目显示在列表中。
虽然这是一个非常简单的例子,但页面也会非常复杂。通常需要处理数据以在视图中显示。例如,您可能会打印一个非常格式化和本地化的日期而不是时间戳。您应该将这样的视图逻辑添加到页面,而不是业务逻辑或模型,它们是面向数据的组件。为避免跨页面的数据处理重复代码,您可以创建一个附加帮助程序组件:
有了这个组件架构和知识,您就可以确保编写干净且结构良好的代码。
对于依赖Web服务和API的任何移动应用程序,离线功能最为重要。用户希望应用程序能够工作,并希望浏览最新的数据状态 - 同样在离线时。
为此,我们提供了一个完整的示例,用于分离可用的模型,视图和逻辑。它访问虚拟休息服务,还包括Storage组件的脱机缓存。你可以在GitHub上找到这个例子的完整源代码: