Do Uno Mvvm?

Last week was a huge week for the Uno platform with their inaugural Uno conference, #UnoConf. As the technology continues to mature, I’ve no doubt that Uno will become a viable solution for building applications to target all sorts of markets. This includes support being progressively added by the various Mvvm frameworks.

Following my previous posts (MVVM Navigation with Xamarin.Forms Shell and MVVM Navigation with Xamarin.Forms Shell – Part II) where I discussed a simple approach to Mvvm with Xamarin.Forms, I figured I’d so something similar with Uno.

Mvvm with Uno

Let’s get on with it and start with a new Uno project – Download and install the Uno Platform Solution Templates extension from the Visual Studio marketplace, if you haven’t already. In Visual Studio, create a new project based on the Cross-Platform App (Uno Platform) project template. I’m going to call the app DoUnoMvvm.

Creating a Class Library

We’re going to separate out our viewmodels and services into a separate library, so add a new project, DoUnoMvvm.Core, based on the Class Library (.NET Standard) project template. Delete the class1.cs and then add a reference to the class library to each of the head projects (i.e. Droid, iOS, UWAP and Wams).

Adjusting NuGet Package References

Right-click on the solution node in the Solution Explorer window and select Manage NuGet Packages for Solution. Go to the Updates tab, check the Include prerelease option and then check the box alongside the packages Uno.Wasm.Bootstrap, Uno.UI, Microsoft.NETCore.UniversalWindowsPlatform and Uno.Core (don’t check either the Logging packages). Click Update to get the latest version of the packages that are checked.

From the Browse tab on the NuGet-Solution window used in the previous step, enter BuildIt.General.Uno into the search box. Select BuildIt.General.Uno and install the packages into all five of the projects.

Mvvm Basics with ViewModelLocator

Now we should be ready to start writing some code. We’re going to keep it simple with the following steps:

  • Create ViewModelLocator class – used for serving up viewmodels and creating services as required
  • Create an instance of ViewModelLocator in App Resources, making it accessible as a static resource in XAML
  • Create MainViewModel class – the viewmodel for the existing MainPage
  • Update ViewModelLocator with a property Main that returns an instance of the MainViewModel class
  • Set the DataContext of the MainPage to use the Main property on the ViewModelLocator
  • Run the application and show data is being served by the MainViewModel.

Here we go…. firstly a new ViewModelLocator class, which is added to the DoUnoMvvm.Core project

public class ViewModelLocator
{
}

Update App.xaml to create an instance of the ViewModelLocator class

<Application
    x:Class="DoUnoMvvm.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DoUnoMvvm"
    xmlns:core="using:DoUnoMvvm.Core"
    RequestedTheme="Light">
  <Application.Resources>
    <core:ViewModelLocator x:Key="ViewModelLocator" />
  </Application.Resources>
</Application>

Now to create the MainViewModel, also in the DoUnoMvvm.Core project. We’ll create a property, WelcomeText, that will return some data to be displayed on MainPage.

public class MainViewModel
{
    public string WelcomeText => "How well do Uno Mvvm?";
}

We need to update the ViewModelLocator class to include the Main property

public class ViewModelLocator
{
    public MainViewModel Main => new MainViewModel();
}

And use this property when setting the DataContext for MainPage. I’ve also updated the TextBlock to be data bound to the WelcomeText property.

<Page
    x:Class="DoUnoMvvm.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DoUnoMvvm"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    DataContext="{Binding Main, Source={StaticResource ViewModelLocator}}"
    mc:Ignorable="d">

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <TextBlock Text="{Binding WelcomeText}" Margin="20" FontSize="30" />
  </Grid>
</Page>

Run the application and there we have it, our first data bound page

Quick Navigation using Event Mapping

That’s pretty much the basics of Mvvm. However, following my previous posts discussing navigation, I just want to demonstrate how to abstract navigation away from both the page and the viewmodel – this allows for more independent testing of viewmodels as there’s no interdependency between viewmodels. Here’s the basic process:

  • Add a new page, SecondPage, that we’re going to navigate to
  • Add a corresponding viewmodel, SecondViewModel, and property, Second, on the ViewModelLocator
  • Update SecondPage to set the DataContext to be bound to the Second property on the ViewModelLocator
  • Add a Button to MainPage that invokes a method, Next, on the MainViewModel
  • Add an event, Complete, to MainViewModel, and raise it from the Next method.
  • Add a mapping to the App.xaml.cs that navigates to SecondPage when the Complete method is raised.

And here’s the code. I’m not going to show you the initial SecondPage as it’s just generated from the template and you’ll see it later anyhow. Instead, we’ll jump to the SecondViewModel (if you’re following along you still need to add the SecondPage to the DoUnoMvvm.Shared project in the Pages folder).

public class SecondViewModel
{
    public string ProgressText => "Now you know how to navigate....";
}

Add the Second property to the ViewModelLocator

public class ViewModelLocator
{
    public MainViewModel Main => new MainViewModel();
    public SecondViewModel Second => new SecondViewModel();
}

Now back to the SecondPage and I have set the DataContext and bound the TextBlock.

<Page
    x:Class="DoUnoMvvm.Shared.Pages.SecondPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DoUnoMvvm.Shared.Pages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    DataContext="{Binding Second, Source={StaticResource ViewModelLocator}}"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid>
    <TextBlock Text="{Binding ProgressText}" />
  </Grid>
</Page>

Now a Button to invoke the transition from MainPage to SecondPage

<Page
    x:Class="DoUnoMvvm.MainPage" ...
    DataContext="{Binding Main, Source={StaticResource ViewModelLocator}}" >
  <StackPanel>
    <TextBlock Text="{Binding WelcomeText}" Margin="20" FontSize="30" />
    <Button Content="Go to Second Page" Click="GoNextClick" />
  </StackPanel>
</Page>

Here we’re simply using a code behind but you could easily use a command. Unfortunately x:Bind doesn’t appear to be working with Uno yet, so you can’t simply bind the Click method to a method on the viewmodel.

public void GoNextClick(object sender, RoutedEventArgs e)
{
    (DataContext as MainViewModel)?.Next();
}

The Next method simply raises the Complete event

public class MainViewModel
{
    public event EventHandler Complete;

    public string WelcomeText => "How well do Uno Mvvm?";

    public void Next()
    {
        Complete?.Invoke(this, EventArgs.Empty);
    }
}

The final step is to add the mapping to App.xaml.cs to define what happens when the Complete event is triggered on the MainViewModel. Add the following property and method to App.xaml.cs, and update the App class to implement the IApplicationWithMapping interface (which comes from the BuildIt.General.Uno library that you should have added earlier)

public IDictionary<Type, IEventMap[]> Maps { get; } = new Dictionary<Type, IEventMap[]>();
private void MapPageTransitions()
{
    Maps.For<MainViewModel>()
        .Do(new EventHandler((s, args) => (Windows.UI.Xaml.Window.Current.Content as Frame)?.Navigate(typeof(SecondPage))))
        .When((vm, a) => vm.Complete += a, (vm, a) => vm.Complete -= a);
}

Invoke the MapPageTransitions method immediately after the Window.Current.Content property has been set equal to a new Frame. In order for the events to get correctly wired up you also need to update both MainPage and SecondPage to inherit from the MappingBasePage class.

Now when you run the application, MainPage will appear with a Button that you can click to navigate to the SecondPage.

Uno How to Mvvm!

You might be thinking…. you’ve just shown me how to do a bunch of UWP code… and that is EXACTLY the point. If you switch to the Droid or iOS or Wasm target, you can run the same application on each of those platforms with NO further code changes required. The Uno platform is about leveraging the skills you already have as a UWP (or as a Xamarin.Forms) developer, allowing you to build rich, high-quality applications for iOS, Android and Web.

Link to the source code

Creating a Flutter App for Web

I’ve covered this topic previously in my post Create, Build and Publish a Flutter Web App but things have changed a little now as web support has been merged, making it easier to build a single application that runs on iOS, Android and Web. To find out more you can check out the Flutter docs for web and Building a web application with Flutter. In this post, I’m going to cover my experience and talk a little about the debugging experience in Visual Studio Code.

With the release of Flutter 1.9 I took a read through the announcement from the Google Developer Days in China and I was initially a little thrown because it indicated that Flutter’s web support has been integrated into the main Flutter repository. I assumed this meant that if I upgraded to 1.9 I would be able to immediately create a Flutter app that targets the mobile and web platforms.

Switching Channel

After upgrading to 1.9 and trying to create a new Flutter project, I quickly realised that I had misunderstood the announcement and that I still needed to switch to a different channel in order to get the integrated experience for Flutter web. So I ran the following command

flutter channel master

After switching to the master channel, I ran:

flutter doctor
flutter upgrade
flutter config --enable-web

New Flutter Project With Web

Once you’ve run these methods, the next thing to do is to create a new Flutter project in Visual Studio code. Press Ctrl+Shift+P to bring up the Command Palette, type Flutter and click Flutter: New Project. After your project is created you’ll notice the addition of a web folder.

Launching Flutter for Web

When you go to launch your Flutter app from within Visual Studio Code you’ll need to decide whether to launch on iOS or Andoird, or the newly supported web. In the lower right corner of Visual Studio Code you can see the current debugging target – in this case it’s already set to use Chrome.

If you tap the debugging target you’ll be prompted to pick an alternative debugging target.

After setting the debugging target, if you press F5 your Flutter app will launch on the appropriate device or emulator. Note that since web support is in preview, there’s no step-through debugging support; you can set breakpoints etc, you just can’t step through the code and inspect variables.

Summary

This was just a short post to draw your attention to the need to switch to the master branch in order to try out the Flutter for web support.

MVVM Navigation with Xamarin.Forms Shell – Part II

Following my previous post on Mvvm Navigation with Xmarin.Forms Shell there were a few things that I felt I hadn’t addressed adequately.

Loading Data on Appearing

The first thing is how to load data when navigating to a page, and perhaps to do something when the user navigates away from the page. Since it’s the responsibility of the viewmodel to provide the data (i.e. via data binding), we have to invoke a method on the corresponding viewmodel when arriving at a page. In Xamarin.Forms the navigation to/from a page invokes the OnAppearing and OnDisappearing methods, which we can use to request that the viewmodel loads data.

The simplest approach is that for each page that needs to load data, the developer can override the OnAppearing method and simply call a method, for example LoadData, on the corresponding viewmodel. Since most pages are likely to have to load some data, this will quickly become a bit of a drag and something we can easily optimise. We’ll introduce an interface, INavigationViewModel, that when implemented by a viewmodel will define methods OnAppearing and OnLeaving. Then in our BasePage (which we introduced in the previous post) we simply need to check to see whether a viewmodel implements the interface before invoking the appropriate method.

public class BasePage : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();

        var vm = this.BindingContext as BaseViewModel;
        if (vm!=null && AppShell.Maps.TryGetValue(vm.GetType(), out var maps))
        {
            foreach (var map in maps)
            {
                map.Wire(vm);
            }
        }

        (vm as INavigationViewModel)?.OnAppearing();
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        var vm = this.BindingContext as BaseViewModel;
        if (vm!=null && AppShell.Maps.TryGetValue(vm.GetType(), out var maps))
        {
            foreach (var map in maps)
            {
                map.Unwire(vm);
            }
        }

        (vm as INavigationViewModel)?.OnLeaving();
    }
}

Note: If you’re going to be adapting some of this code for your project, you might want to consider making OnAppearing/OnLeaving return a Task that can be awaited.

Passing Parameter

The next point that we need to cover is how to pass a parameter from one page to the next. In our example, we have a list of items and when the user taps on an item the app navigates to the ItemDetailPage to view the details of the item. This requires some information about the selected item to be passed to the ItemDetailPage.

I’ve seen all sorts of mechanisms for passing data between pages across various application platforms. Some platforms only allow a simple query string to be passed as part of navigating between pages, whilst others allow you to pass entire objects. In the case of Xamarin.Forms the default navigation pattern doesn’t provide for a mechanism to pass data. However, because you create the instance of the new page before navigating to it, there’s a perfect opportunity to pass data from the existing page to the new page.

The following code is adapted from the code in the previous post to illustrate wiring up navigation to the ItemDetailPage based on the SelectedItemChanged event on the ItemsViewModel. Note that the selected item, passed into the event handler as the variable args, is set on the Item property of the ItemDetailViewModel.

// When the SelectedItemChanged event is raised on the ItemsViewModel
// navigate to the ItemDetailPage, passing in a new ItemDetailViewModel
// with the selected item
Maps.For<ItemsViewModel>()
    .Do(new EventHandler<Item>(async (s, args) =>
    {
        if (args == null)
        {
            return;
        }
        var page = new ItemDetailPage();
        if (page.BindingContext is ItemDetailViewModel vm)
        {
            vm.Item = args;
        }
        await Navigation.PushAsync(page);
    }))
    .When((vm, a) => vm.SelectedItemChanged += a, (vm, a) => vm.SelectedItemChanged -= a);

Note: Setting a property on the viewmodel will occur before the OnAppearing method is invoked (see previous discussion regarding loading data) which means the viewmodel can make use of whatever data you pass in when loading data. In this case we could have simply passed in the Id of the item we want to display and have the viewmodel load the rest of the data related to that item.

Returning a Parameter

One thing I’ve seen in a number of MVVM frameworks is the ability to navigate to a new page with the expectation that the page will return data at some point in the future. Whilst there are cases where this is convenient (eg prompting the user for some data) this pattern introduces a very heavy dependency between the lifecycle of two pages and their corresponding viewmodels.

An alternative is to assume that each page/viewmodel is independent and that when you arrive at a page any data that is displayed on the page should be refreshed. Of course, if you’ve got a long list of items that the user has scrolled mid-way down and you reload the list, it’s going to return to the top of the list, making the user experience quite nasty. Furthermore, if the list of items is coming from a service, you don’t want to be reloading that data every time the user goes back and forth to a details page.

Most of the above issues can be handled with appropriate caching of data in a service or manager class. If the user navigates to an item, makes changes and saves that item, the cached data in the service would be updated (along with any call required to any backend services). When the user returns to the list of items, the service would simply return the latest cached data. This addresses the latency issue of having to fetch data from a backend service but how do we address the scroll position issue?

One way is to only update the items in the list that have changed (ie catering for add, edit, delete of items). However, having to write this code for every page is again tiresome. In the past, I investigated a possible workaround for this issue when I discussed immutable data – check out posts I, II and III on working with immutable data

Summary

Again, the disclaimer here is that this isn’t a library that you can just drop in and use in your application. However, feel free to take/leave what code you find useful.

Get the latest source code

Flutter: Text Widget

In this post we’re going to look at the Text widget in Flutter and some of the options you can tweak when displaying text within your app. If you’re after a more detailed discussion of strings, characters and how they’re displayed, you should check out the post, Mastering Styled Text in Flutter. To get into it, we’re going to start off with a new Flutter project, which already displays text to indicate how many times the button has been pressed. The text is broken into two Text widgets to allow for the styling of the actual counter value to be different from the preceding text.

Text Constructor

This code snippet shows us a couple of things: firstly, that the first parameter of the Text constructor is the string to be displayed; secondly, we can override, or set, the style of the text using the style parameter. This, of course, prompts the question as to what parameters are there on the Text widget and what do they mean.

Let’s take a look at the Text constructor – select the Text widget and press F12 in Visual Studio Code to be taken to the actual code for the Text class and its constructor:

class Text extends StatelessWidget {
  const Text(
    this.data, {
    Key key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
  })

As we can see from the constructor definition, there is a single data parameter, followed by a list of optional parameters. Let’s step through these parameters and take a look at what they do.

Text.style

The first optional parameter of the Text widget is “style” which is of type TextStyle. As you’d imagine the TextStyle class can be used to specify the foreground and background color, the font size and weight, letter and word spacing, locale, shadows and much more.

What’s interesting is that the first Text widget shown above, we didn’t specify a TextStyle, and yet the text was still displayed on the screen with a basic style applied. This is because when the style property isn’t set, the Text widget will search the widget tree looking for style information to use. To demonstrate this, try passing a Text widget into the runApp method.

void main() => runApp(Text("test"));

This will actually fail to run stating that RichText widgets require a Directionality widget ancestor. We can fix this easily by wrapping the Text widget with a Directionality widget as follows:

void main() => runApp(Directionality(textDirection: TextDirection.ltr, child: Text("test")));

After doing this, what we’re left with on-screen is a very unstyled piece of text situated in the top left corner of the screen.

Returning now to our application, the question becomes, where does the default styling for Text come from. The answer to this question can be found in the documentation for the style property (press F12 in VS Code to navigate to the property definition where you can find the relevant documentation). The documentation states that the style property will be merged with the style associated with the closest enclosing DefaultTextStyle (assuming the inherit property on the style is set to true). What this means is that the Text widget will traverse up the widget hierarchy looking for a DefaultTextStyle widget on which to base the style of the text on.

So the next question you’re probably going to ask is where is the DefaultTextStyle widget being added to the hierarchy because it doesn’t appear anywhere in the app that was generated when we created the new project. Well, it might surprise you to know that the DefaultTextStyle widget appears at least twice, being added by both the MaterialApp and Scaffold widgets respectively. The following diagram illustrates just part of the widget hierarchy that shows the DefaultTextStyle appearing below the AnimatedDefaultTextStyle node.

Coming back to our Text widget, if we specify a value for the style property, it will be merged with the style of the nearest DefaultTextStyle widget. For example, let’s change the colour of the text to purple.

Text(
  'You have pushed the button this many times:',
  style: TextStyle(color: Colors.purple),
),

Alternatively, as we saw in the second Text widget in the initial example, the style of the Text widget can be set based on the current Theme. The Theme defines a number of different text styles that can be used – check out the Flutter documentation for more information on the individual text styles. In the following code, the display1 style is applied to the Text widget.

Text(
  '$_counter',
  style: Theme.of(context).textTheme.display1,
),

Sometime you’ll want to use one of the theme text styles but you want to adjust one or more attributes. For this, you can use the apply method as in the following example that sets the colour of the text to purple.

Text(
  '$_counter',
  style: Theme.of(context).textTheme.display1.apply(color: Colors.purple),
),

Since we’ve now set the colour of both Text widgets to purple, it would be good to be able to extract this so that it’s only applied once and will affect both Text widgets. This comes back to what I was saying about the DefaultTextStyle widget and how the style property merges with the nearest DefaultTextStyle widget – in order to set an attribute that should apply to all Text widget, we just need to add a DefaultTextStyle widget to the hierarchy, setting the appropriate attribute.

child: DefaultTextStyle(
  style: TextStyle(color: Colors.purple),
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text(
        'You have pushed the button this many times:',
      ),
      Text('$_counter', style: Theme.of(context).textTheme.display1),
    ],
  ),
),

Note that the DefaultTextStyle widget also merges its style with the style of the nearest parent DefaultTextStyle widget.

Text.strutStyle    

Setting the strutStyle property gives you the ability to fine-tune the separation between rows of text. For example, if you have a number of Text widgets that have differing font style and sizes, you can specify the strutStyle to ensure the same spacing between each row.

Text(
  'This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  style: TextStyle(fontSize: 12),
  strutStyle: StrutStyle(fontSize: 13),
),
Text(
  'This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  style: TextStyle(fontSize: 14),
  strutStyle: StrutStyle(fontSize: 13),
),
Text(
  'This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  style: TextStyle(fontSize: 12),
  strutStyle: StrutStyle(fontSize: 13),
),

As you can see from the following image, despite the text in the middle section being a slightly larger font, the line separation is uniform across all lines of text.

Further information on using the strutStyle property can be found at the StrutStyle documentation

Text.textAlign and Text.textDirection

I’ve grouped the textAlign and textDirection properties together as they are related as the textDirection determines how some textAlign values control the layout of text in the Text widget. In the following example there are five Text widgets, with different combination of textAlign and textDirection property values.

Text(
  'JUSTIFIED - This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  textAlign: TextAlign.justify,
),
Text(
  'LEFT (LTR) - This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  textAlign: TextAlign.left,
  textDirection: TextDirection.ltr,
),
Text(
  'LEFT (RTL) - This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  textAlign: TextAlign.left,
  textDirection: TextDirection.rtl,
),
Text(
  'START (LTR) - This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  textAlign: TextAlign.start,
  textDirection: TextDirection.ltr,
),
Text(
  'START (RTL) - This is a very long piece of text designed to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
  textAlign: TextAlign.start,
  textDirection: TextDirection.rtl,
),

When we run this code we can see that the textAlign property does what you’d expect it to – the text is justified for the TextAlign.justify value and aligned left for TextAlign.left. What’s interesting is that TextAlign also includes values start and end which are important if you’re considering supporting RTL languages within your application. As you can see from the following image with textAlign set to TextAlign.start, if we adjust the textDirection between LTR and RTL we can see that the text changes from left aligned to right aligned.

Text.locale

One of the most common reason for explicitly setting the locale for a Text widget is to adjust the text that is being rendered. Adjusting the locale will change the way certain unicode characters are displayed.

Text.softWrap

Setting the softWrapp property to false will disable wrapping, causing the text to be truncated by the right edge of the parent container.

Text.overflow

The overflow property controls what happens when there is more text than will fit in the space available. For example you can use the predefined value TextOverflow.ellipsis for Flutter to insert … when there isn’t sufficient space. Note that ellipsis will disable wrapping, regardless of how much space there is available (see maxLines discussion below).

Another possible overflow value is TextOverflow.fade

Container(
  height: 30,
  child: Text(
    'This is a very long piece of text designed verylongwordwithnospaces to wrap over multiple lines. This is a very long piece of text designed to wrap over multiple lines.',
    overflow: TextOverflow.fade,
  ),
),

Using TextOverflow.fade in conjunction with a fixed height on the parent Container rejusts in the following effect where the first Test widget fades out before the second Test widget.

Text.textScaleFactor

The textScaleFactor can be set to apply an arbitrary scaling to the text being displayed by the Text widget. Note that this will override any textScaleFactor applied by the current MediaQuery, so may result in incorrect layouts on devices where the textScaleFactor isn’t null or 1.0.

Text.maxLines

Where the text being displayed stretches over multiple lines, the maxLines property defines the maximum number of lines that will be displayed.

When the overflow property is set to TextOverflow.ellipse the maxLines property can be set to increase the number of lines of text that will be displayed before the ellipses are appended.

Text.semanticsLabel    

You can use the semanticsLabel property to improve the way that screen reader and assistive technologies work with your application. Check out this post on Semantics for further information.

Text.textWidthBasis

The textWidthBasis property can control how the width of the Text widget is defined. More information is available via the documentation.

Summary

In this post, I’ve walked through the use of the Text widget. I would highly recommend pressing F12 on a Text widget and go explore the source code. It’s worth to note that other classes, such as RightText and TextSpan, that exist – I’ll leave it to the reader to drill into these and understand how they’re related.