使用Atomic Design,F#和Fabulous创建漂亮的Xamarin Forms应用程序

在阳光明媚的日子里,显示一个笨拙的同龄人和海洋

极好 允许以功能样式使用F#编写Xamarin Forms应用程序。神话般的灵感来自 榆树,这证明通过使用模型视图更新(以下简称MVU)模式功能语言非常适合编写UI代码。尽管功能代码在空值和竞态条件周围压缩了许多潜在的错误-在本文中,我们将不专注于Fabulous的这一方面。相反,让我们看一下如何创建可以在将来保持可维护状态的精美UI。

这篇博客文章是Xamarin UI July的一部分,由 史蒂文·塞维森。一定要检查我所有的美丽帖子 共同作者.

精选#XamarinUIJuly徽章

受史蒂文斯(Stevens)以前关于-我喜欢称呼-可滑动UI的一些帖子的启发。我想说明为什么用代码编写UI可以使您编写不仅美观而且易于维护和扩展的UI。所以对于这篇文章,我将实现我发现的设计思想 运球 经过 味觉.

应用程序设计,如盘点所示

尽管此博客文章将重点介绍Fabulous,但在使用C#和XAML编写应用程序时,您可以应用相同的原理。但是您最终将获得一堆文件,而且感觉会更加复杂。 F#是一门简洁的语言,而Fabulous允许使用比C#和XAML通常所需的更少的代码行来编写应用程序。我并不是说这就是您退出Fabulous的原因。但是事实是……如果您是F#的新手,那么Fabulous接下来是一个简短的介绍。如果这是您所有的老新闻,请随时跳过简介。

极好简介

Let’s start with the good old Welcome to Xamarin Forms blank app:

module App = 
    type Model = 
      { Message : string } // your apps state, we could do without...

    type Msg = 
        | SomeStateChange // just for the demo, we do not need this...

    let initModel = { Message = "Welcome to Xamarin.Forms!" }

    let init () = initModel, Cmd.none

    let update msg model =
        match msg with
        | SomeStateChange -> model, Cmd.none

    let view (model: Model) dispatch =
        View.ContentPage(
          content = View.StackLayout(
            children = [ 
                View.Label(text = model.Message, horizontalOptions = LayoutOptions.Center, verticalOptions = LayoutOptions.CenterAndExpand)
            ]))

    // Note, this declaration is needed if you enable LiveUpdate
    let program = Program.mkProgram init update view

type App () as app = 
    inherit Application ()

    let runner = 
        App.program
#if DEBUG
        |> Program.withConsoleTrace
#endif
        |> XamarinFormsProgram.run app

Yes, this is all the code you would usually have in your blank C# app. We will not go into too much detail on how all the functions work. At the bottom, you can see the type App, which translates to the App.xaml.cs class, i.e. the entry point of any Xamarin Forms app. Our analogue to the MainPage is the module App. The three components of the MVU pattern are present with the Model (an F# record, if your new to F# think of it as a POCO, not quite the same but close enough for now) and the view and update functions.

更新功能是处理视图的所有更改的地方。仅显示文本,此功能实际上没有任何作用。由于稍后我们将重点介绍UI,因此我将向您简要介绍更新功能在普通应用中的功能。想象一下您的所有UI更改和后台任务事件都必须进行 依序 通过这一点。定义了所有状态更改。您可以重现应用程序的每种状态-哦,没有比赛条件

The view function contains the ContentPage, which includes a StackLayout and a Label. At first, you might not think much about it. But look how terse it is written. For example, the StackLayout children , that is a simple list in F#. So adding another element to the grid would be simply adding a new UI element.

The functions get invoked by Fabulous and do not interact with Xamarin Forms directly. This is important to understand because this means that all of the code you write can be 100% unit tested. All the dependencies to the view are resolved within the Fabulous framework. The view function returns the instructions on how to create the UI, but it does not create it. If you change a value such as the welcome message, the Fabulous Framework checks what parts have changed and updates the view accordingly. The React.JS framework uses the same technique with a shadow DOM (Document Object Model) that is then taken to update the actual UI.

原子设计和编码用户界面

Writing your UI with code comes with a few perks. While you could write all of your UI in the view function. It might get a bit hard to view at a glance over time. But being only code, you can split up the code into different functions. This also allows you to reuse parts of the UI in different places. And reusability reusing/combining components is at the heart of Atomic Design.

原子设计

虽然独特的设计,可重用的组件听起来很棒,但让我们来看看如何使用Fabulous设计这样的应用程序。我们想从基本元素(原子)开始,然后将它们组合到更重要的UI组件中,最后是Page。

当我们查看应用程序的设计时,我们可以看到大多数标题标签似乎都具有相同的字体。另一个迅速引起我注意的UI组件是持有目的地描述和要做的事情的卡片:

目的地描述

现在,我们可以看到标题具有相同的字体,粗体,并且与“要做的事情”部分中的卡片相同,并且具有相同的字体大小。因此,让我们创建一个函数,该函数允许我们使用参数text和font size创建标题标签:

let titleLabel text fontSize =
    View.Label(text = text,
        fontSize = fontSize,
        textColor = textColor,
        verticalOptions = LayoutOptions.Center,
        fontAttributes = FontAttributes.Bold)

目的地是通过图片(如果我可以这么说的话,是漂亮的图片!)以及城镇,国家,等级和收藏夹的简短说明来显示的。似乎收藏夹应该是可单击的,因此我们假设它是一个按钮。可能类似于右上角的搜索按钮。牢记可访问性,如果用户需要进行交互,那么我通常更喜欢使用按钮或平台交互控件。这样,使用屏幕阅读器可以更轻松地优化体验。因此,我们需要带有图标的按钮-或文本。由于Xamarin Forms允许我们使用自定义字体,因此我们可以使用诸如 字体很棒 为我们提供可扩展的图标。一定要签出 詹姆斯的帖子 有关如何在Xamarin Forms应用程序中使用Font Awesome的信息。因此,让我们创建一个给定图标,颜色,背景色和命令功能的函数,并通过按钮返回给我们:

let materialFont =
    (match Device.RuntimePlatform with
                             | Device.iOS -> "Material Design Icons"
                             | Device.Android -> "materialdesignicons-webfont.ttf#Material Design Icons"
                             | _ -> null)

let materialButton materialIcon backgroundColor textColor command =
    View.Button(text = materialIcon,
        command = command,
        fontFamily = materialFont,
        fontSize = 20.,
        backgroundColor = backgroundColor,
        widthRequest = 42.,
        textColor = textColor)

现在是描述文字,即国家/地区。让我们再次创建一个函数,该函数将在给定文本的情况下创建标签:

let descriptionLabel text =
    View.Label(text = text,
        textColor = secondaryTextColor,
        fontSize = descriptionFontSize
        )

您是否注意到页面的“要做的事情”部分重复了标题和描述模式。到现在为止,我们已经创建了原子设计所称的原子。现在,将其中一些原子打包成一个连贯的块,例如(分子):

let titleAndDescription title titleFontSize description =
    View.StackLayout(margin = 0.,
        children=[
            titleLabel title titleFontSize
            descriptionLabel description |> fun(label) -> label.Margin (Thickness(0.,-8.,0.,0.))]

这将使我们可以重复使用标题& Description duo further. Also, note that we had to adjust the margin a bit. You can think of the |> as a pipe forward. Since we have a View type, we can pipe it forward to a lambda function where we change the margin. Calling the margin function will again return a View type. If you are using LINQ, you most probably have joined multiple calls to where select et al. - we are doing the exact same thing here.

Now looking back at the short description of the destination, we can also see a rating of the city with stars. So let’s create a function that given the icon and text colour returns a Label based on font awesome.

let materialIcon materialIcon color =
    View.Label(text = materialIcon,
        textColor = color,
        fontFamily = materialFont,
        fontSize = 18.,
        verticalOptions = LayoutOptions.Center,
        fontAttributes = FontAttributes.Bold)

评分栏-我认为它是一个只读指示器,可向我显示总体评分(介于0到5之间)。给定4.5的评级,我们希望有四颗全星,一颗被一半覆盖。因此,让我们将此控件拆开,假设我们想要一个仅以一定百分比绘制星形的函数:

let ratingStar percentage =
    let star = materialIcon star starColor
    let boxViewWidth = 16. - (16. * percentage)
    View.Grid(
        padding = 0.,
        margin = Thickness(0.,-4.,0.,0.),
        children = [
            star
            View.BoxView(color = backgroundColor, 
                widthRequest = boxViewWidth,
                isVisible = (if percentage > 0. then true else false),
                horizontalOptions = LayoutOptions.End)
            ])

该函数又名star factory由另一个函数调用,该函数根据给定的等级绘制N个星:

let ratingControl (rating:decimal) =
    let fullNumber = Math.Ceiling(rating)
    let fraction = (rating - Math.Truncate(rating))
    View.StackLayout(orientation = StackOrientation.Horizontal,
        children = [
            for i in 1m .. fullNumber -> if i = fullNumber then ratingStar (float fraction) else ratingStar 1.
        ])

Now we have all of our building blocks together for the description, but we still have the image with rounded corners left. A quick look at the ImageView from Xamarin Forms tells us: “No rounded edges.” But when putting the image in a Frame, we can create the rounded edges effect. So let’s create a function that gives us an image with round corners:

let roundedCornerImage imagePath =
    View.Frame(cornerRadius = cornerRadius,
        padding = 0.,
        isClippedToBounds = true,
        hasShadow = true,
        content = View.Image(
            source = imagePath,
            aspect = Aspect.AspectFill)
    )

现在将所有零件组装好,让我们组装它们,以使我们获得带有圆角并由简短说明覆盖的图像:

let cityDescriptionFrame city dispatch =
    View.StackLayout(
        margin = Thickness(16.,0.,16.,0.),
        children = [
            (roundedCornerImage city.Image |> fun(img) -> img.HeightRequest 320.)
            View.Frame(
                heightRequest = 70.,
                margin = Thickness(24.,-64.,24.,0.),
                padding = Thickness(20.,12.,16.,12.),
                backgroundColor = Color.White,
                cornerRadius = cornerRadius,
                content = View.Grid(
                    rowdefs=["auto"; "auto" ],
                    coldefs=["*";"auto"],
                    children=[
                        (titleAndDescription city.Name titleFontSize city.Country)
                        (favoriteIcon city dispatch).GridColumn(2)
                        (ratingControl city.Rating).GridRow(1).GridColumnSpan(2)
                        ]
                ),
                hasShadow = true)
        ])

同样,我们可以实现“要做的事情”部分。很棒的事情是我们可以重用我们已经创建的许多组件。然后,我们可以将所有部分放到view方法中,该方法为我们提供以下UI:

应用截图

您可以在上找到整个样本 的GitHub.

Side notes: No, we are not required to have all the code in one file. But since this is a one-pager application, I left it together, so it is easier to navigate the code in a browser. Further note that the CarouselView did not work correctly when I was working with the view. I hope I will be soon able to get it working and have a sample which will allow switching between cities as intended by design.

结论

将Atomic Design模式应用于您的UI确实可以使您的应用程序更易于维护和创建。鉴于Fabulous允许用代码编写UI,因此无需太多样板代码即可创建自定义且一致的UI相对简单。 More Fabulous提供了实时更新功能,使您可以在调试会话期间实时编码。 UI不仅可以适应,而且逻辑也可以执行。您可以阅读更多有关 实时更新功能 在官方网站上。

似乎在2019年的最近几天里,用代码编写UI的方式重新流行起来。像Apple这样的公司正在开发Swift UI。如果您是顽固的C#爱好者,则应查看 瑞安·戴维斯(Ryan Davis)发表 Xamarin用C#编写UI的内容。

您可以在下面阅读有关Atomic Design模式的更多信息 Brad Frosts网站.

Updated: