Connecting Flutter to Azure at MS Ignite | The Tour – Sydney

This year I was lucky enough to be able to present at Microsoft’s Ignite | The Tour, which has just concluded here in Sydney. What amazed me was that I got to present on a fledgling technology recently released by Google, Flutter. Of course, this by itself wouldn’t have been sanctioned at a Microsoft event, so the important part was that I was presenting on connecting Flutter to Azure, and highlighting just how powerful the Azure options are for mobile app developers on any platform.

Using Flutter to develop cloud enabled mobile applications

Flutter is one of the newest cross-platform mobile application development frameworks and brings with it the ability to generate high-fidelity applications that look amazing on every device. This session begins with a very brief overview of Flutter, covering the tools and resources required to get started. The remainder of the session connects a Flutter application to key Azure services such as Identity and App Center. The key takeaway from this session is how Azure accelerates the development of mobile applications irrespective of what technology or framework the app is developed with.

In this post I’m going to walk through the demo portion of the session, which revolved around the creation of a very simple task style application.

Getting Started – New Project

Let’s get started by creating a new project in VS Code (If you haven’t already got the Flutter SDK installed, go to https://flutter.io and follow the Get Started button in top right corner). In VS Code in you press Ctrl+Shift+P and type Flutter you’ll see all the available options.

image

Select “Flutter: New Project” and you’ll be prompted to give your project a name – I was short on inspiration for a name for the project, hence “World’s Best Flutter App”. Note that the name of your project needs to be all lowercase, no spaces or special characters.

image

You’ll need to select a folder to put your new project in, and then VS Code will generate a default application that consists of a basic UI which includes a button that increments a counter on the screen. We’re going to start by removing most of this and replacing it with:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'World\'s Best Flutter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'World\'s Best Flutter App'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
    );
  }
}

As with all new projects, at this point I’d highly recommend running the application to make sure everything has been correctly created and that you have a running application – there’s nothing worse than investing hours into crafting an amazing solution only to have to debug it for hours because of some issue that happened early in the process. At this point there’s not too much to the application other than the title bar

image

Basic Task List

The first piece of functionality we’re going to add is the ability to enter a tasks and see them appear in a list. To do this we need a text entry (TextField widget) and a list (ListView widget) and we’re going to present them with the text entry above the list in a vertical stack (Column widget). Before adding the widgets I’m going to add a couple of instance variables to the _MyHomePageState class (note that this is the state that goes with the MyHomePage stateful widget):

class _MyHomePageState extends State<MyHomePage> {
  //// Controller for TextField and List of tasks ////
  final TextEditingController eCtrl = new TextEditingController();
  List<String> tasks = [];

Now we can add our widgets to the body property on the Scaffold widget:

return Scaffold(
  appBar: AppBar(title: Text(widget.title)),
  ////  Body of the app is made up of a single column ////
  body: new Column(
    children: <Widget>[
      ////  TextField to capture input ////
      new TextField(
        controller: eCtrl,
        onSubmitted: (text) {
          tasks.add(text);
          eCtrl.clear();
          setState(() {});
        },
      ),

      ////  Expand ListView to remaining space ////
      new Expanded(
        child: new ListView.builder(
          itemCount: tasks.length,
          itemBuilder: (BuildContext ctxt, int index) {
            return Padding(
              padding: EdgeInsets.fromLTRB(10, 10, 5, 5),
              child: Text(tasks[index]),
            );
          },
        ),
      ),
    ],
  ),
);

A couple of things to note here:

  • It appears that I’ve added a couple of trailing commas – this is by design and lets the auto formatter know to position things on new lines. This is really important as Flutter code can get really messy if you don’t keep an eye on the layout of the code
  • Pressing Shift+Alt+F in VS Code will apply the auto formatter and makes the code much more readable – take a look at the following image to see how VS Code presents the above code
image

At this point we have a working app where a user can enter a task and it’s added to a list of tasks that is presented in the lower area of the screen. There are plenty of online tutorials etc that talk about layout and how setState works so I’m not going to cover that here. If you’re unfamiliar with Flutter/Dart code I would suggest at this point taking the time to go and understand what’s going on in the code – we’re going to build on it, so it’s important you understand how things work before we move on.

Visual Studio App Center

Before adding any more functionality to the app, I want to first take a side step to discuss the use of Visual Studio App Center which has been specifically created by Microsoft to support app developers, providing capabilities for building, testing, distributing apps, along with services such as analytics, crash reporting and push notifications. Currently support for Flutter is via a third party plugin which only provides analytics and crash reporting but we can expect this to grow as more developers take on Flutter and put pressure back onto Microsoft to provide official support, similar to what’s already provided for React Native and other cross platform technologies.

For our tasks app we’re going to make use of the analytics and crash reporting, as well as the support for distributing the application via App Center. To do this we first need to go to App Center and register two applications: one for iOS and one for Android. Got to either https://mobile.azure.com or https://appcenter.ms and sign in using your preferred credentials – my preference is to use my Office 365 (ie Azure Active Directory) account as this plays nicely into the way I sign into the rest of Azure and makes things like billing etc really easy. After creating your account, if you locate the apps list, then in the top right corner you should see an Add new button, which gives you the option to Add new app

image

In the Add new app flyout, you need to provide an App name and provide information about the OS and Platform – these are more important if you’re going to use App Center to build the application but is also relevant to the distribution process, so it’s important that you set these correctly (Note: Adam Pedley has a great walk-through of using App Center for build and distribution of Flutter apps). Since the App name needs to be unique within your App Center instance, it makes sense to use some sort of platform identifier as part of the name so it’s easy to identify the iOS and Android apps in the list.

image

After creating the app, you’ll be presented with some platforms specific getting started documentation. Most of this can be ignored as it’s not relevant to our Flutter application. However, you do need to extract the id that’s been assigned to your application. This can be found as part of the code samples eg:

image

After creating the app registrations in App Center, we now need to add in the Flutter plugin into our application and initialize it with the app id. Note that because App Center issues a different app id for each platform, you need to include some conditional platform logic to determine which app id to use. There are three plugins you need to add to your pubspec.yaml file under the dependencies section:

All packages are based on the source code at https://github.com/aloisdeniel/flutter_plugin_appcenter developed by Aloïs Deniel and Damien Aicheh

dependencies:
  flutter:
    sdk: flutter
  appcenter_analytics: ^0.2.1
  appcenter_crashes: ^0.2.1
  appcenter: ^0.2.1

After adding in the packages, we also need to import the relevant types to the top of our main.dart file:

////  Imports for platform info ////
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/foundation.dart' show TargetPlatform;

////  Imports for App Center ////
import 'package:appcenter/appcenter.dart';
import 'package:appcenter_analytics/appcenter_analytics.dart';
import 'package:appcenter_crashes/appcenter_crashes.dart';

Next we need to initialise the App Center plugin but adding code to the _MyHomePageState class

class _MyHomePageState extends State<MyHomePage> {
  ////  Identifier used for initialising App Center ////
  String _appCenterIdentifier = defaultTargetPlatform == TargetPlatform.iOS
      ? "3037d80f-XXXXXXXXXXX-adb968c67880"
      : "f6737675-XXXXXXXXXXX-be38b29b0f89";

And then a call to the AppCenter.start method when the class initialises – we’re overriding the initState method to call this logic when the instance of the _MyHomePageState is first created:

////  Override Initialise State ////
@override
void initState() {
  super.initState();
  initAppCenter();
}

////  Initialise AppCenter ////
void initAppCenter() async {
// Initialise AppCenter to allow for event tracking and crash reporting
  await AppCenter.start(
      _appCenterIdentifier, [AppCenterAnalytics.id, AppCenterCrashes.id]);
}

The last thing we’re going to do is to add a call to the trackEvent method for when an item is added to the tasks list. For this we’re going to modify the onSubmitted callback on the TextField

onSubmitted: (text) {
  ////  Tracking event ////
  AppCenterAnalytics.trackEvent("Adding item", {"List Item": text});

Now when you launch the app, and start adding tasks you’ll see some tracking of app related activity appear within the Analytics tab of App Center:

Note: If you get errors at this point, you may need to stop and restart your application to make sure the newly added packages are correctly built and deployed as part of your application. Hot reload doesn’t always handle this well.

image

You can see information about events raised by the app

image

Including the ability to drill down into the event and see more information, in this case you can see the List item that were added to the task list – note that this is an application specific key-value combination specified as part of the call to trackEvent.

image

 

Build and Release Automation

Before we can start distributing our app via App Center we typically setup a build and release (aka a DevOps) pipeline for our application – at Built to Roam this is one of the first things we encourage all our clients to do as it significantly cuts down on the manual steps involved, resulting in fewer errors and less wasted time. For this, we rely on Azure DevOps, and again a big shout out to Aloïs Deniel who’s created an extension for Azure DevOps – https://marketplace.visualstudio.com/items?itemName=aloisdeniel.flutter

image

After adding this extension to your Azure DevOps tenant, you can easily setup a build and release pipeline – there are a few steps involved in this, so I’ll keep this for a follow up post. The build steps involve tasks for Installing Flutter, Building the app with Flutter, Copying the generated app to the artefacts folder and the Publishing the artefacts. The release steps simply involves uploading the generated app to App Center. You can of course adjust these to add functionality and perhaps target different environments for testing, and of course uploading to the relevant stores directly from your release pipeline.

Update 22/9/2019: There are two other extensions for Azure DevOps for working with Flutter. The first simply makes it easier to install the Flutter SDK. The second is an update to the extension Aloïs created that adds AAB support – if you want to use this extension, make sure you uninstall the original extension Aloïs created because the two conflict and appear as a duplicate.

Once you have your build and release pipelines in place, you should start to see your releases appear in the Distribute / Releases tab in App Center

image

If you open App Center on your iOS or Android device you can install each release for testing.

Azure App Service

Right now we actually have a pretty useless app since all it does is record tasks in memory. When you restart the app, your list of tasks will be gone. Plus there’s no way to have your list of tasks appear on another device etc. We’re not going to complete the entire app in the blog post but what we do want to do is show how easily you can connect a Flutter app to some backend services hosted in Azure.

First up, let’s take a look at the backend service we’re going to connect to. In the demo I used an ASP.NET Core application that was published to an Azure App Service but I could just have easily used a Functions app. The application consistent of a single controller that currently only supports a Get operation to retrieve a list of tasks, which for the moment are hardcoded.

[Route("api/[controller]")]
[ApiController]
public class TasksController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<Task>> Get()
    {
        return new[]
        {
            new Task {Title = "Task 1", Completed = true, },
            new Task {Title = "Task 2", Completed = false,},
            new Task {Title = "Task 3", Completed = true, },
            new Task {Title = "Task 4", Completed = false,},
            new Task {Title = "Task 5", Completed = true, },
        };
    }
}

public class Task
{
    public string Title { get; set; }
    public bool Completed { get; set; }
}

 Swagger

It’s also worth noting that in the Startup.cs of this application I added in the logic to generate Swagger information for the service (You need to add a NuGet reference to Swashbuckle.AspNetCore.Swagger, Swashbuckle.AspNetCore.SwaggerGen and Swashbuckle.AspNetCore.SwaggerUi):

public class Startup
{
    …
    public void ConfigureServices(IServiceCollection services)
    {
        …
        services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Simple ToDo", Version = "v1" });
            });
        …
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        …
        app.UseSwagger();
        app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Simple ToDo V1");
            });
        …
    }
}

Update 12/11/2019: If you’re using .NET Core 3 preview, you’ll need to upgrade to the preview of Swashbuckle NuGet packages and change the Info class reference to OpenApiInfo. Although at time of writing, there is an issue with the Dart code generator based on the swagger generated by .NET Core 3 preview, so perhaps best to stick with .NET Core 2.2 until v3 is officially released.

This application was published to a new Azure App Service which meant that both the /api/tasks endpoint, as well as the swagger endpoints were publically available. We’ll come back to securing these in a bit. To be able to call the tasks endpoint we could have gone ahead and created a raw http request and then have to worry about decoding the returned json into a series of entities, for which we’d have to write, or generate, the decoding logic. Instead, I went over to SwaggerHub and imported the swagger endpoint:

image

After selecting “Import and Document API” I entered the url for the swagger endpoint on the Azure App Service eg https://simpletodonick.azurewebsites.net/swagger/v1/swagger.json

image

After hitting Import SwaggerHub will convert your json formatter swagger into YAML – we’re not really interested in this piece, since what we’re after is some generated Dart code. However, before we can go ahead and generate the Dart code we first have to configure the generator options for Dart. Click on the Export button in the top right of the SwaggerHub page, followed by Codegen Options.

image

Locate “dart” in the left hand tree, and then set the browserClient property to false – by default the Dart generator creates code suitable for Dart being used for web development. For Flutter you need to set this property to false.

image

Going back to the Export menu, now select “dart” from under the Client SDK option.

image

This will automatically generate and download a package that will include a strongly typed set of classes for accessing the tasks endpoint. I extract this folder and place the contents of the generated zip file into a new sub-folder called “tasks” at the root of the Flutter application.

image

After adding the files into the folder, I then needed to update the pubspec.yaml file for my application to include the package as a dependency, as well as the http package:

dependencies:
  flutter:
    sdk: flutter
  appcenter_analytics: ^0.2.1
  appcenter_crashes: ^0.2.1
  appcenter: ^0.2.1
  http: '>=0.11.1 <0.12.0'
  swagger: 
    path: tasks 

(note that “swagger” correlates to the name of the imported package which is defined with the pubspec.yaml file within the tasks folder)

Update 22/9/2019: There’s an incompatibility between a new Flutter project and the generated Dart code. To fix it you need to update the pubspec.yaml inside the tasks folder to include the same environment value as the pubspec.yaml for your application.

Now to make use of the generated code in our application, I also needed to add a imports statement to the main.dart file.

////  Import for Swagger ////
import 'package:swagger/api.dart';

As we’re going to be using the Task entity described by the Swagger I need to update the list variable from a List<string> to List<Task>. I also added the base url for the app service.

////  Change List to use Task ////
List<Task> tasks = [];

////  Base url for the Simple ToDo service ////
final _baseUrl = 'https://simpletodonick.azurewebsites.net';

At this point I also needed to fix up the existing task list functionality which expected a string, to use a Task entity. This was in two places, firstly when creating a new task I needed to create a new Task:

tasks.add(
  Task()..title = text,
);

And then when displaying the tasks in the list

child: Text(tasks[index].title),

Now with the app back up and running, we can go ahead and call the service in order to retrieve the tasks when the app launches.

void loadData() async {
  ////  Calling App Service ////

  // Setup api client with base url and token returned by auth process
  var client = ApiClient(basePath: _baseUrl);

  var tasksApi = new TasksApi(client);

  var downloadedTasks = await tasksApi.callGet();
  setState(() {
    tasks = downloadedTasks;
  });
}

The loadData method should be called from the initState method, just after calling initAppCenter. Running the app at this point should return a list of tasks on startup and then allow subsequent tasks to be added (of course at the moment those tasks aren’t saved anywhere!)

Authentication with Azure B2C

Currently there is no security applied to the app service, which is clearly not only bad practice, it also means that it is impossible to personalise the task list to the individual user, since everyone will be returned the same list of tasks. The good new is that we can add authentication to our service without writing a single line of code. To do this we’re going to choose to use Azure B2C. However, Azure App Services can also use a variety of different social credential providers to authenticate the user.

For this application I created a new instance of Azure B2C and setup the default B2C_1_signupandin user flow (aka policy). After creating the instance and the user flow, you still need to associate the Azure B2C instance with the app service and vice versa. To do this, start by creating a new application registration within the Azure B2C portal – go to Applications tab and click Add button.

image

In creating the application registration we want to enable both web app/api and native client. You’ll notice that under the Native Client area there is a Custom Redirect URI specified. To do authentication using the flutter_appauth plugin (which I’ll come to in a second) the Custom Redirect URI should be in a similar format ie <reverse domain>://oauthredirect.

After creating the application registration, take note of the Application ID – you’ll need this when we update the Authentication on the App Service that requires a Client Id.

The next thing to do, also in the Azure B2C portal is to navigate to the User flows (policies) tab and then click the Run user flow button.

image

From the Run user flow dialog, copy the link at the top (eg https://worldsbestflutterapp.b2clogin.com/worldsbestflutterapp.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signupandin)

Now we need to switch across to the App Service in the Azure Portal and click on the Authentication tab. Initially App Service Authentication will be set to Off and there is a warning that anonymous access is permitted.

image

Switch the App Service Authentication to On and under Authentication Providers, click through on the Azure Active Directory settings.

image

Use the Application Id captured when setting up the Azure B2C as the Client ID. Use the link recorded from the Run user flow dialog as the Issuer Url. Click OK to commit the Azure Active Directory settings.

Back in the Authentication / Authorization settings, make sure that the “Action to take when request is not authenticated” dropdown is set to “Log in with Azure Active Directory” – this will enforce sign in if no Authorization token is presented.

image

After hitting Save, if you attempt to access the app service you’ll find that you get a security exception (if sending raw requests), or redirected to the sign in prompt if attempting to load the endpoint in a browser.

 Authentication in Flutter using Flutter_AppAuth

After setting up authentication on the app service, if you attempt to run the application it won’t be able to retrieve the list of tasks as the requests are unauthenticated. In order to create a request that is authenticated (ie supplies a validate Authorization header in this case) we need to get the user to sign into the application. Luckily there is a Flutter plugin, flutter_appauth (source code), that has been created by Built to Roam colleague Michael Bui, that wraps AppAuth iOS and Android libraries.

image

There are a few steps involved in getting our application setup to use flutter_appauth. We’ll start by adding the package reference to the pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_appauth:
  ...

Next, we need to upgrade the Android application to use the AndroidX libraries instead of the older support libraries. To do this we need to open the Android part of the flutter application in Android Studio. Right-click on the android folder in VS Code and select the Open in Android Studio option.

image

When prompted to “Import Project from Gradle” you can accept the defaults and click Ok. The application may take a few minutes to open in Android Studio and you’ll need to allow a bit of extra time for Android Studio to finish processing the application before the Migrate to AndroidX option becomes enabled in the Refactor menu:

image

Side note: The first couple of times I tried this whilst preparing this demo Android Studio kept saying that there was nothing to update. It turns out that there was an update to the Flutter plugin to Android Studio that was waiting to be installed – I did this, which resulted in Android Studio reloading. After reloading the folder structure under Project expanded to show the referenced flutter plugins as well as any external libraries. At this point running Migrate to AndroidX worked.

When Migrate to AndroidX does work for you it’s not immediately obvious that something has changed…. which is because it hasn’t. Instead Android Studio will have opened up a tool window at the bottom of the screen, prompting you to confirm the Refactoring Preview. Click Do Refactor to complete the migration.

image

If you look in the /android/app/build.gradle file you should see that the dependencies list at the end of the file have been updated.

dependencies {
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0-alpha'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha'
}

At the point of writing this post the migration process references alpha versions. You can update this to reference the stable versions now:

dependencies {
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
}

Whilst in the build.gradle file the compiledSdkVersion and targetSdkVersion should be increased to at least 28. Lastly, update the defaultConfig element to include manifestPlaceholder

defaultConfig {
    applicationId "com.example.worldsbestflutterapp"
    minSdkVersion 16
    targetSdkVersion 28
    ...
    manifestPlaceholders = [
            'appAuthRedirectScheme': 'com.builttoroam.simpletodo'
    ]
}

Note that the appAuthRedirectScheme should match the reverse domain part of the Custom Redirect URI property set when setting up Azure B2C earlier.

For iOS open the /ios/Runner/Info.plist file and add the CFBundleURLTypes key/value as follows.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	...
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>com.builttoroam.simpletodo</string>
			</array>
		</dict>
	</array>
</dict>
</plist>

We’re now ready to write the code to authenticate the user within the application. Start by adding an imports to bring in flutter_appauth

////  Import for Flutter_AppAuth ////
import 'package:flutter_appauth/flutter_appauth.dart';

Next, add in some constants that are needed for the authentication process to the beginning of the _MyHomePageState class, as well as an instance of the FlutterAppAuth class.

class _MyHomePageState extends State<MyHomePage> {
  ////  Attributes required for authenticating with Azure B2C ////
  final _clientId = '7c57c3a6-bedb-41d6-9dcd-318224013a26';
  final _redirectUrl = 'com.builttoroam.simpletodo://oauthredirect';
  final _discoveryUrl =
      'https://worldsbestflutterapp.b2clogin.com/worldsbestflutterapp.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signupandin';
  final List<String> _scopes = [
    'openid',
    'offline_access',
  ];

  ////  Instance of FlutterAppAuth ////
  FlutterAppAuth _appAuth = FlutterAppAuth();

At the beginning of the loadData method we created earlier, add a call to the authorizeAndExchangeCode method on the _appAuth instance.

void loadData() async {
////  Authenticate with Azure B2C ////
  var result = await _appAuth.authorizeAndExchangeToken(
      AuthorizationTokenRequest(_clientId, _redirectUrl,
          discoveryUrl: _discoveryUrl, scopes: _scopes));

Now, the only thing left to do is to pass in the token that gets returned from the authentication process into the call to retrieve the tasks. To do this we actually need to make a minor change to the swagger generated code to tell it to use OAuth credentials. Locate the api_client.dart file, which for my project is at /tasks/lib/api_client.dart and change the ApiClient constructor as follows:

ApiClient({this.basePath: "https://localhost"}) {
  // Setup authentications (key: authentication name, value: authentication).
  _authentications['Bearer'] = new OAuth();
}

This sets the ApiClient to use OAuth which means that you can then call the setAccessToken method on the instance of the ApiClient, which should be the code that immediately follows the call to authorizeAndExchangeCode.

var client = ApiClient(basePath: _baseUrl);
client.setAccessToken(result.idToken);

And there you have it, your application should be good to go. When you launch the application, it will redirect to the Azure B2C sign in page. After authenticating it will redirect back to the application and load the task list.

image

Big shout out to the community for rolling so many great plugins for Flutter. The world of cross platform development is progressively becoming easier to make high quality applications through frameworks such as Xamarin.Forms and Flutter.


Contact Built to Roam for more information on building cross-platform applications


4 thoughts on “Connecting Flutter to Azure at MS Ignite | The Tour – Sydney”

  1. How did you get flutter_appauth into you app? The example references it in a parented directory. Is that the way it must be done? Is there not a way to reference it with a version number like other packages? I keep throwing the below error which seems to point to mismatches in versions:
    I/zygote64(20302): Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/customtabs/CustomTabsServiceConnection;

    Reply
  2. Hi Nick,

    Since this post was written, do we have the push notification support for flutter apps available now? Can’t find much help on the topic.

    TIA

    Reply
  3. I’m new to Flutter and semi-new to Azure devops. I’m writing a mobile app and went with Flutter after a lot of research.

    To date: I had figured out a lot of what’s in this tutorial on my own.

    But MAN this is great information! I’d missed a lot.

    So thank you for so much concise and useful data. More than once as I read this I said “oh that’s how you do it!”

    Reply

Leave a comment