Blazor vs Flutter - Part 4

This is the fourth article in a 10 article series: Blazor vs Flutter. This post focuses on components and pages.

This is the fourth article in a 10 article series: Blazor vs Flutter.

  1. Blazor vs Flutter - Part 1 (Overview)
  2. Blazor vs Flutter - Part 2 (Project Setup)
  3. Blazor vs Flutter - Part 3 (Package Installation)
  4. Blazor vs Flutter - Part 4 (Components & Pages) (You are here)
  5. Blazor vs Flutter - Part 5 (State Management)
  6. Blazor vs Flutter - Part 6 (Navigation)
  7. Blazor vs Flutter - Part 7 (API Requests)
  8. Blazor vs Flutter - Part 8 (Authentication)
  9. Blazor vs Flutter - Part 9 (Deployment)
  10. Blazor vs Flutter - Part 10 (Multi-Platform)

In the previous article we installed some required packages, so now we can start adding functionality. First, we'll create a component, and then we'll add that component to a page which will be used to display weather information in part 7

Flutter

In Flutter, there doesn't seem to be much of a distinction between pages and components. Everything is a "widget," so I typically create a "pages" folder for things related to a particular screen, page, or area, while using a "components" folder for things that are used in multiple places throughout the app.

Let's start with a component to display a line of text that will be used for the location, temperature, and weather.

import 'package:flutter/material.dart';

class TextLine extends StatelessWidget {
  
  final String textContent;
  final double textSize;
  final double padding;
  final bool bold;
  
  const TextLine({
    super.key, 
    required this.textContent, 
    required this.textSize, 
    required this.padding,
    this.bold = false,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: padding),
      child: Text(
        textContent,
        style: TextStyle(
          fontWeight: bold
            ? FontWeight.bold
            : FontWeight.normal,
          fontSize: textSize
        ),
      ),
    );
  }
}

text_line.dart

Notice that we're defining the widget as stateless, meaning that it won't have any internal state and, if the values need to change, the widget will be re-created with the new values.

We're providing 4 different properties within the constructor that are then used to return a padded text string.

Now we can create a page to display all of the weather information, and use this widget there.

import 'package:flutter/material.dart';
import 'package:flutter_app/components/text_line.dart';

class WeatherPage extends StatefulWidget {
  const WeatherPage({super.key});

  @override
  State<WeatherPage> createState() => _WeatherPageState();
}

class _WeatherPageState extends State<WeatherPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Weather'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            TextLine(
              textContent: 'Location Placeholder', 
              textSize: 24, 
              padding: 8,
              bold: true,
            ),
            TextLine(
              textContent: 'Temperature Placeholder', 
              textSize: 20, 
              padding: 8
            ),
            TextLine(
              textContent: 'Weather Placeholder', 
              textSize: 20, 
              padding: 8
            ),
            Padding(
              padding: EdgeInsets.only(top: 8),
              child: Icon(
                Icons.sunny,
                size: 36,
              ),
            )
          ],
        )
      ),
    );
  }
}

weather_page.dart

This time, we created a stateful widget because we'll need to store information about the location which will then be passed to the various TextLine widgets. Being stateful just means that it can have internal state that changes over time, which is also why there are actually two classes, instead of just one like before.

In order to see the new page, we need to add some navigation. That's going to come in a future article, so for now we'll just repace our "home" screen with the newly created weather page.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // home: const MyHomePage(title: 'Flutter Demo Home Page'),
      home: const WeatherPage()
    );
  }
}

main.dart

FlutterPage

Blazor

Now we can do the same thing in Blazor. Similar to Flutter, Blazor apps are built using razor components, which could act either as a page, or an individual component.

First, we'll create the same TextLine component we did in flutter:

@inherits TextLineBase

<div 
    style="font-weight: @FontWeight; font-size: @(TextSize)px; padding: 0 @(Padding)px;"
>
    @TextContent
</div>

TextLine.razor

using Microsoft.AspNetCore.Components;

namespace BlazorApp.Components
{
    public class TextLineBase : ComponentBase
    {
        [Parameter]
        public string TextContent { get; set; }

        [Parameter]
        public int TextSize { get; set; }

        [Parameter]
        public int Padding { get; set; }

        [Parameter]
        public bool Bold { get; set; } = false;

        protected string FontWeight { get; set; }

        protected override void OnInitialized()
        {
            FontWeight = Bold
                ? "bold"
                : "normal";
        }
    }
}

TextLineBase.cs

Notice that with Blazor, we're just creating an html file for the display and a regular C# class for the logic. You do have the option of creating everything in a single file, but my preference is to keep things separated. If you wanted to combine them, it would look like this:

<div 
    style="font-weight: @FontWeight; font-size: @(TextSize)px; padding: @(Padding)px 0;">
    @TextContent
</div>

@code {
    [Parameter]
    public string TextContent { get; set; }

    [Parameter]
    public int TextSize { get; set; }

    [Parameter]
    public int Padding { get; set; }

    [Parameter]
    public bool Bold { get; set; } = false;

    protected string FontWeight { get; set; }

    protected override void OnInitialized()
    {
        FontWeight = Bold
            ? "bold"
            : "normal";
    }
}

TextLine.razor

Next, we create the weather page:

@using Components.TextLine
@using MudBlazor

@inherits WeatherPageBase

@page "/weather"

<PageTitle>Weather</PageTitle>

<div id="container">
    <BlazorApp.Components.TextLine.TextLine 
        TextContent="Location Placeholder"
        TextSize="24"
        Padding="8"
        Bold
    />
    <BlazorApp.Components.TextLine.TextLine 
        TextContent="Temperature Placeholder"
        TextSize="20"
        Padding="8"
        Bold
    />
    <BlazorApp.Components.TextLine.TextLine 
        TextContent="Weather Placeholder"
        TextSize="20"
        Padding="8"
        Bold
    />
    <div style="width: 36px">
        <MudIcon Icon="@Icons.Material.Filled.WbSunny" />
    </div>
</div>

WeatherPage.razor

#container {
    display: flex;
    flex-direction: column;
    height: 100%;
    justify-content: center;
    align-items: center;
}

WeatherPage.razor.css

using Microsoft.AspNetCore.Components;

namespace BlazorApp.Pages.WeatherPage
{
    public class WeatherPageBase : ComponentBase
    {
    }
}

WeatherPageBase.cs

You can see, we're creating the same files for the page as we did with the component. The only difference is that a css file is also included to get the styling we want.

Since Blazor is build with regular html and css files, the page is only as high as elements by default. That means we need to make a few changes to main layout to have our weather information centered like in the Flutter app.

First, we create a javascript file to calculate the available hight of our content area by subtracting the height of the nav-bar from the height of the screen

function getScreenHeight() {
    const windowHeight = window.innerHeight;
    const navBarElement = document.getElementById('nav-bar');

    return navBarElement
        ? windowHeight - navBarElement.offsetHeight
        : windowHeight
}

helpers.js

Then, we update index.html to include our script

<body>
    ...

    <script src="js/helpers.js"></script>
</body>

index.html

Finally, we update the main layout to dynamically change the height of the element

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
		...

        <article class="content px-4" style="@_contentHeight">
            @Body
        </article>
    </main>
</div>

@code {
    [Inject]
    private IJSRuntime _jsRuntime { get; set; }

    private string _contentHeight { get; set; } = string.Empty;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender)
        {
            try
            {
                var contentHeight = await _jsRuntime.InvokeAsync<int>("getScreenHeight");
                _contentHeight = $"height: {contentHeight}px";
                StateHasChanged();
            }
            catch (Exception ex) { }
        }
    }
}

MainLayout.razor

Blazor has the ability to make use of dependency injection, which we're using here to get an instance of the IJSRuntime that allows us to call javascript functions.

There are also various lifecycle methods that we can tap into, and in this case we're waiting until the screen has rendered in order to get the available height. At that point, we set a style variable and call StateHasChanged(), which will cause a re-render with our updated style.

To view the new page, we can just add /weather to the browser url.

BlazorPage

Conclusion

It's relatively simple to create a component in Flutter as well as Dart. The "better" option just comes down to preference I think.

  • Flutter is nice because you're just using Dart. Blazor, on the other hand is using html, css, javascript, and C#. If you're already doing web development, that may not be an issue though.
  • Blazor has built-in dependency injection, an easy way to run javascript in the browser, and makes it easy to separate UI and logic (Flutter & Dart may have these capabilities, I've just never seen or used them)

I do feel like building a layout or screen to a specific spec is easier in Flutter, but that could also be due to the fact that I prefer working on the backend and have never been much of a fan of css. There are some component libraries that make it easier though, like MudBlazor, Blazorise, and Ant Design Blazor.

In the next article, we'll take a look at state management.