Moving on from Xamarin Forms to MAUI

With the release of .net 7 and MAUI, I was thinking about performing an upgrade from Xamarin Forms to MAUI, as Xamarin Forms will be at end of life on 1-May 2024. I thought it would be appropriate to upgrade my first example on Xamarin Forms to MAUI.

A few links that I found on upgrading:

I mostly used the first link in my migration, so I’ll start my upgrade in that order. If you’d like to go along with me, this was my starting point.

Note: If you are on windows or are using windows on parallels, you may be interested in the upgrade assistant.

Conversion start

To see an end version of where I wanted to go, I generated out a scaffolded “Welcome to MAUI” project, so I could see what the destination was to look like. I referred back to this many times, so I advise you to do this as well.

The version 1 of SQLiteSample hadn’t been updated since 2018; so I needed to get it up to date under Xamarin Forms; so I went and did that in upgading the nugets; with the majority of the actual code changes occurring with the Fluent validation that broke with an upgrade a version or two behind it’s present version. I tested and then committed the changes and started going to MAUI.

From the guide, I performed the changes on the common project’s .csproj file and on the Android and iOS project’s .csproj file from the instructions. Unfortunately this caused these errors:

Error NETSDK1139: The target platform identifier android was not recognized. (NETSDK1139) (SQLiteSample)
Error NETSDK1139: The target platform identifier ios was not recognized. (NETSDK1139) (SQLiteSample)

This caused me to look at the instructions closer, as the instructions were for .net 6, not .net 7 which I was using. I switched the labels over to .net7, but this didn’t help. I looked here and it showed that I might be missing some of the components. The workload list showed what was installed and what was available. I did these installations to continue:

dotnet workload list

sudo dotnet workload install maui
sudo dotnet workload install maui-android
sudo dotnet workload install maui-ios 

Note: sudo is for Mac and not needed for Windows

I then performed the replaces and removes. One item not noted in this list was this:

Replace all instances of On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(true) with On<iOS>().SetUseSafeArea(true)

At this point I compared my scaffolded solution to the converted solution and then created the missing appropriate directories in the shared project and if there were files and those files which were not in the common or iOS / Android project solutions, I copied them over from the scaffolded solution into the current project.

Conversion points not covered in the checklist

At the end of that I compiled, but it said that the AndroidManifest file does not exist. I found a note here, and ended up copying the manifest into the common solution.

Compiled again and now it’s complaining about Application is an ambiguous reference. I then placed ‘Microsoft.Maui.Controls.’ prefix in front of Application to specify what version to use.

Next up was this error:

Error NETSDK1047: Assets file '{dir}/SQLiteSample/SQLiteSample/obj/project.assets.json' doesn't have a target for 'net7.0-ios/iossimulator-x64'. Ensure that restore has run and that you have included 'net7.0-ios' in the TargetFrameworks for your project. You may also need to include 'iossimulator-x64' in your project's RuntimeIdentifiers. (NETSDK1047) (SQLiteSample)

At this point, I ended up moving the iOS / Android project specific items into the common project; and then unloading the iOS / Android projects. While I didn’t think of this at the time that this error occurred, I didn’t look at the common .csproj file to see if some additional changes were automagically put into the file, as later on I did notice Visual Studio added additional changes to the .csproj file.

This occurred next:

Error: The MinimumOSVersion value in the Info.plist (15.2) does not match the SupportedOSPlatformVersion value (11.0) in the project file (if there is no SupportedOSPlatformVersion value in the project file, then a default value has been assumed). Either change the value in the Info.plist to match the SupportedOSPlatformVersion value, or remove the value in the Info.plist (and add a SupportedOSPlatformVersion value to the project file if it doesn't already exist).

I had to go in and change the supported os platform version under common project properties to 15.2.

Following that, the compile got much further along, but this showed up:

actool error : None of the input catalogs contained a matching stickers icon set or app icon set named  "AppIcon".

I found this article that talked about the new MAUI icon set; note: there is not a build action named ForegroundFile as noted in this article. The common Info.plist file (copied over from the iOS project) and the scaffolded project’s Info.plist had the same line for XSAppIconAssets and neither solution had that directory. The build action on all 3 of the files was set to the same thing as on the scaffolded project; so I’m not sure why it didn’t work.

I decided to copy over the scaffolded solution’s Info.plist, to keep things moving. In this case, there’s nothing special about the Info.plist nor the solution. When that was done, I compiled and it worked – very odd. I created the values that were not in this file from the old Info.plist.

Converting Xamarin Forms platform specific dependency injection over to MAUI’s platform specific code calls as shown here; I tried continuing to use the interface, instead of the specific partial method name, but it didn’t work.

Conversion database issues

At this point, the app compiled and I started it on the iOS emulator. Sadly, it didn’t work and died with this error: System.TypeInitializationException: The type initializer for ‘SQLite.SQLiteConnection’ threw an exception. That lead me to this link, which in turn lead me here. Apparently, now it’s not possible to use the sqlite-net-pcl nuget alone, but must use it with other nugets, even w/o encryption. From the link, add these:

  • SQLitePCLRaw.core – version 2.1.0 (or higher)
  • SQLitePCLRaw.bundle_green – version 2.1.0 (or higher)
  • SQLitePCLRaw.provider.dynamic_cdecl – version 2.1.0 (or higher)
  • SQLitePCLRaw.provider.sqlite3 – version 2.1.0 (or higher) – note NOT lib.e_sqlite3!

In addition to the nugets, have to add this line to the AppDelegate:

raw.SetProvider(new SQLite3Provider_sqlite3());

As I noted in this article, sqlite-net-pcl doesn’t work well in a multi-threaded way, I converted it over to a singleton instance and used dependency injection to be sure that there was only 1 copy of the class repository which held a reference to that database class. I tried to use the new MAUI dependency injection, but it didn’t work how I needed it to work, so I turned to using Splat locator DI instead.

Wrap up

With that I tested out the functionality of both iOS and Android and both worked as expected.

Finally, I reloaded the unloaded old (and now unused) iOS Android projects and removed them from the solution and deleted them from my hard drive. Note: After I removed and physically deleted the projects; I had to close, reopen, clean and build the solution and visual studio Mac 2022 to get rid of the odd error that was occurring.

The updated SQLiteSample solution is on Github.

Hopefully, this will help folks out who get odd errors as I did. The best to luck to us all who need to say thank you and farewell to Xamarin and head to MAUI.

Android – App restructuring part 2 – Kotlin networking

In my first post about modernizing my native app, I mixed both Java and Kotlin. I wanted a 95% or more Kotlin application; with that unless the app isn’t complex, that app has to use Kotlin coroutines. I started working on an application that would call my already existing library that would get the data. While I was able to do this, I wasn’t able to get down to exactly why things were working the way that they were doing so when examining the log. Because of that, I decided to go a bit more basic and create a even more simplified app that would get data from a RESTful-like source and show the result. I didn’t care about the data, other than it was an actual network call; it needed to take real time to perform and had the capability of being interrupted or turned off. I went out looking for another API to use. I found an API out there which would cover these bases which is free and doesn’t need API keys: Quotable. I got the idea from this article.

An observation on Android Studio – given the Android architectural guidelines, I thought that a new app would scaffold out a generic app with view, view model and repository, it doesn’t – certainly an interesting choice not to do that. In any event, I created the app: a Retrofit 2 helper, a quote service, a quote repository to handle getting those quotes, the quote models, the activity, view model and view model factory.

My code is here at github.

Kotlin coroutine

What is a coroutine anyway?

A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.

Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.

Coroutines basics, retrieved 17-Jun-22

Unlike Java, where one would create a threaded class extension to perform long running or network based activity, in Kotlin, you can create a coroutine to do the same type of work, excepting without the pain of setting up and running your threaded work. This will generally make development easier and faster; but there are some things to keep track of and I’ll point them out. I’d recommend reading this article on coroutines with Android specifics.

I’ll not be going over my coroutine code within my app, as there are some pretty good articles out there on coroutines including from Jetbrains here, and another from Android / Google.

Network status check

The first thing I wanted to handle is gaining / loosing network connection. This has been changed / deprecated over the various API versions with the latest change under API 29. I came up with two different ways to do that check.

  1. Context extension. I’m using this to set the initial network status in the view model from the activity. I found this Stack Overflow article which had the basis for the extension solution.
  2. ConnectivityManager callback helper. I’ve used a BroadcastAdapter for this in the past, but that’s been partially deprecated, so time to move to a ConnectivityManager. I found a good blog entry on this. There’s a slight problem with the blog entry version, but I’ve fixed it in my implementation. The error with their implementation is that the same network can be added multiple times to the network list; not so with my version. This uses LiveData for communication, which is fine, as a Android context is required for the call requiring access to either the activity or the Application context. This is the code from the activity that deals with the handling of the network state helper:
       NetworkStatusHelper(this).observe(this, {
            val status = it == NetworkStatus.Available

            when(it) {
                NetworkStatus.Available -> {
                    setNetworkText(true)
                }
                NetworkStatus.Unavailable -> {
                    setNetworkText(false)
                    if (viewModel.getDataFetchStarted() &&
                        !viewModel.getDataFetchCompleted() &&
                        !viewModel.getDataFetchNetworkCompleted()
                    ) {
                        viewModel.cancelJob()
                    }
                }
            }
        })

LiveData vs. Kotlin Flow

Given we are talking about LiveData and Kotlin Flow, let’s define what they are:

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

Android Jetpack, retrieved 17-Jun-22

In coroutines, a flow is a type that can emit multiple values sequentially, as opposed to suspend functions that return only a single value. For example, you can use a flow to receive live updates from a database.

Flows are built on top of coroutines and can provide multiple values. A flow is conceptually a stream of data that can be computed asynchronously. The emitted values must be of the same type. For example, a Flow<Int> is a flow that emits integer values.

Kotlin flows on Android, retrieved 17-Jun-22

I’ve been reading up on activities, view models and repositories and I came across these blog entries: Don’t use LiveData in Repositories and No more LiveData in Your Repository: There are better options. What they point out does intuitively make sense; especially if you plan to create unit tests – you don’t want to tie Android architecture components to a non-Android class. However, there are some things to consider on switching over from a LiveData to a Flow. I’d like to emphasize with Flows – while it’s in the documentation here, it didn’t quite register for me until later:

Flows are cold streams similar to sequences — the code inside a flow builder does not run until the flow is collected.

Asynchronous Flow, retrieved 17-Jun-22

For more background on Flow, I recommend this article that was written by the Flow’s author and is a good read; for how Flow and coroutines work together, read this article.

From Quote repository:

    @WorkerThread
    fun initializeDataSource() {
//        job = scope?.launch {
        scope?.launch {
            networkResults = getData()
        }
    }

The reason for the commented out line is that essentially, the Flow variable (networkResults) contains nothing from the getData fun (method), as it has not been collected / consumed. While you can get the job from the launch, the job’s isCompleted status will be true almost immediately and thus not worth bothering with here. To interrogate the job’s isCompleted and other statuses within the Flow, you must assign it when the Flow actually starts to be collected.

In this app, where the networkResults is collected / consumed is in the view model, via the startLiveDataCollection fun which calls the collectData fun that processes the emit‘s or exceptions generated in the quote repository’s getData fun. This loads whatever we got back – data, error or an exception – into a LiveData variable bound from the activity. This activity code that deals with the resulting LiveData starts with this line:

viewModel.networkResults.observe(this, {

Due to how things work in Android, I’d like to point out two things. The startLiveDataCollection and collectData fun’s are run on the main thread, as UI items should only be updated on the main thread. The network fetch Flow in the quote repository’s getData fun is run on a worker (in this particular case the Dispatcher.IO) thread for two reasons:

  • On Android networking cannot be done on the main thread
  • Since this work can take a long time, the processing should be moved off of the main thread to allow the main thread to continue to smoothly update the UI as well as avoid UI jank

I’d recommend annotating your fun’s using the appropriate threading model so that the compiler can examine your code and throw a warning / error, if it is incorrectly used.

Lets go over the Flow from the quote repository:

            job = currentCoroutineContext().job

            val quotesService = RetrofitHelper.getInstance().create(QuotesService::class.java)

            for (netTry in 1..3) {
                try {
                    yield()
                    for (i in 5 downTo 1) {
                        networkFetchComplete = false

                        results = quotesService.getQuotes(i)

                        if (results != null && results.isSuccessful) {
                            emit(NetworkResult.Success(results))
                        } else {
                            emit(NetworkResult.Failure(results))
                        }

                        delay(5000);
                        yield()
                        networkFetchComplete = true
                    }
                } catch (uhe: UnknownHostException) {
                    emit(NetworkResult.Exception(uhe))
                    // cancel the flow
                    currentCoroutineContext().cancel()
                } catch (io: IOException) {
                    emit(NetworkResult.Exception(io))
                    // cancel the flow
                    currentCoroutineContext().cancel()
                } catch (e: Exception) {
                    throw e
                }

                if (networkFetchComplete) {
                    break
                }
            }

At the very top the assignment of the class job variable is done from the currentCoroutineContext‘s job, so that the job status in the view model can be properly interrogated to see what is going on. As I want this to be cancelable, I’ve got yield sprinkled through here to add places where it can be cancelled. As with other types of coroutine commands, cancellation is cooperative so placement of commands, such as yield, where it can be cancelled is required. The Flow then tries to get the data from the quote service and if it doesn’t throw, it will emit the successful response with data or not successful response with no data. If it throws, then for known exception types, it will emit the exception for a general exception, it will re-throw it. If it gets a UnknownHostException or IOException it will cancel itself. A twist on this, if exception catch job cancels are commented out and the app will either (dependent on timing):

  • Network Status helper from the main activity is triggered with no network available. It then calls the cancel job in the quote repository through the view model. This is most likely from a timing perspective.
  • Will emit the exception on the flow and go back to the view model and activity, show the error and then bullet #1 will be hit.

I’ve got the delay in there to allow me to turn off the emulator’s networking to see what happens – otherwise the networking part normally too fast for just one network call. I’d like to have moved some of the duplicate emit‘s from those exception handlers out of this to a single fun, but when I did so, the emit‘s show as an error in that fun; therefore I’m stuck with same boilerplate littered throughout. The networkFetchComplete variable dealt with if the code was able to make it all of the way through the 5 quote fetches and if so, drop out of the outer loop and allow the view model to check it’s status.

What Did I learn?

The goal of this app is to learn how to get data and process it. In that, the goal was achieved and ready to transfer that knowledge back to obtaining data for my application. Dealing with flows is / will be tougher than just the coroutine networking and I think that will be where the challenge lies. My application library, already written in Java already does the heavy work with the networking, and original processing of the data; I’ll need to properly assimilate that processing into a repository. That is the next step in the app refactoring.

What would be next steps for this app – if it was continued?

If I was expanding this to experiment further, I’d deal with these areas:

  • In the quote repository, in the netTry outer loop of getData fun, I’d add additional logic to handle the specific network failures to delay / network check for starting / continuing of the quotes
  • Given that I wasn’t able to start / continue with the first bullet, to await the network reconnection and start the processing.
  • Add a local and remote data source in the repository, so that it would be the same as the Android architectural model

Wrap up

Hopefully this gave you some insight into what and how I went about learning about Kotlin coroutines and Flows. I didn’t find an app example quite like this, so hopefully it will be of use to you.

Xamarin Forms – Data retrieval with a network service call

I am currently working through refactoring my native Android application; with that I realized I had never posted about networking using Xamarin using an ObservableCollection. I wanted to see how an actual active network call worked vs. my prior example working with Xamarin ObservableCollection lists using data created by the user in Xamarin Forms – Group ListView using an ObservableCollection. I updated first solution in there to use Forms 5 and to use the current android API.

In the network call, I wanted it to use a REST endpoint, as most endpoints have converted over to it and I wanted to use a endpoint that didn’t require a API key to keep it simpler for everyone. This turned out to be more difficult than I thought it would be. I found two: pokeapi.co, which is all about all Pokemon and openbrewerydb.org that dealt with public information on breweries, cideries, and brewpubs. I ended up using openbrewerydb.org, as that didn’t require multiple calls to piece enough information together to display.

Note: With iOS, you’ll have to either have an Apple developer account or use this method to sign the iOS application, so the app network call will work – otherwise it will not work and throw an error.

Service mapping

I used Postman to explore the JSON results of this call: https://api.openbrewerydb.org/breweries?by_state=ohio&page=5. I ended up choosing Ohio as it had more than 4 pages worth of data. I used https://json2csharp.com and pasted the results into it and it generated out a class:

    public class Root
    {
        public string id { get; set; }
        public string name { get; set; }
        public string brewery_type { get; set; }
        public string street { get; set; }
        public object address_2 { get; set; }
        public object address_3 { get; set; }
        public string city { get; set; }
        public string state { get; set; }
        public object county_province { get; set; }
        public string postal_code { get; set; }
        public string country { get; set; }
        public string longitude { get; set; }
        public string latitude { get; set; }
        public string phone { get; set; }
        public string website_url { get; set; }
        public DateTime updated_at { get; set; }
        public DateTime created_at { get; set; }
    }

The website didn’t quite decode the JSON properly, as it should have made a list instead of a single element; also the object items should have been strings instead – but most of the data for these were null, so that’s OK. To be fair, it created the class w/o any input from me and I was able to fix it easily enough. I ended up calling this class Brewery and put it into the model directory.

Service Call

Next was time to do the service. I’ve seen this example, but ended up using this blog entry as my primary network example. I used the above open brewery link as my URI base and formatted the data parameters into the URI. Once that is done, access the network and then process the result, if it worked OK. If this were a production app, I’d do a few retries, in case the result wasn’t successful.

        public const string BASE_BREWERY_LIST_URL = "https://api.openbrewerydb.org/breweries?by_state={0}&page={1}";

        public static async Task<IList<Brewery>> GetBreweriesAsync(string state, int page)
        {
            IList<Brewery> breweries = new List<Brewery>();

            string url = String.Format(BASE_BREWERY_LIST_URL, state, page);
            using (HttpClient httpClient = new HttpClient())
            {
                System.Diagnostics.Debug.WriteLine("Get data - starting network call");
                var response = await httpClient.GetAsync(url);
                System.Diagnostics.Debug.WriteLine("Get data - network call complete");

                if (response.IsSuccessStatusCode)
                {
                    var content = await response.Content.ReadAsStringAsync();
                    var posts = JsonConvert.DeserializeObject<IList<Brewery>>(content);
                    breweries = posts;
                }
                else
                {
                    httpClient.CancelPendingRequests();
                }
            }

            return breweries;
        }

XAML

Next was to deal with the XAML. As I noted above, I updated my Github fork of the Xamarin forms sample project that used ObservableCollection. In this project in the XAML, there were warning messages complaining about items were not bound. Since they worked fine, it was bound correctly, it was just done via code behind binding here:

BindingContext = new ViewModelSection(Navigation);

I ended up commenting this binding out and moving the binding into to the XAML by adding the viewModel label:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="LabelledSections.LabelledSectionXaml"
             xmlns:viewModel="clr-namespace:LabelledSectionsList;assembly=LabelledSectionsList">

and adding this chunk (as this is a ContentPage), which :

    <ContentPage.BindingContext>
        <viewModel:ViewModelSection />
    </ContentPage.BindingContext>

This required a slight change to the view model to add a constructor that didn’t have any parameters.

Strangely, this took me adding and removing the text ViewModelSection probably 3-4 times as well as cleaning / rebuilding each time and closing / reopening VS Mac a few times before the XAML stopped showing the view model name (ViewModelSection) as not an error. This is one reason to leave the code behind binding 🤨

Jumping back to the network project I wanted to have a top block that dealt with getting the data and below it a list of the data that was brought back. I specifically wanted a count of the number of rows currently in the ObservableCollection brewery list. I originally had this as a separate count in the view model, but changed it to use the actual count of the brewery list for accuracy. I again performed the XAML binding, similar to the other project noted above. As I was not doing an update, I wanted to be sure to set the binding as one way on the list. The caching strategy was from the default construction of the ContentPage, so after looking at the docs, it seemed to be the best of the given strategies w/o moving to another renderer.

            <ListView x:Name="MyListView" 
                    ItemsSource="{Binding LocationList, Mode=OneWay}"
                    CachingStrategy="RecycleElement">

Interestingly enough, the use of Forms 5 allowed me to use XAML hot reload – I had not used it much before. Also interesting is that while running it via VS, both under iOS and Android, it ran pretty janky; but run just on it’s own, it ran fine. I thought that this might be an issue with the thread locking ObservableCollection when adding items to the list, but even after creating a subclass of the collection, it was still janky. I ended up removing the NotifyPropertyChange as ObservableCollection has this built in; but the jank continued. Given this I wonder if there’s a problem with XAML hot reload to cause this – just not sure. Given the advice to use ObservableCollection instead of a normal list, I went ahead and stuck with that, but in this particular case, it might be smarter to move to a list for a possible performance increase as it’s not a two way binding and we aren’t checking for network updates to items in our list.

UI – iOS / Android

I’m glad to have finally done this for comparison. To be sure, it’s much easier than dealing with native Android network calls, but less customizable.

If you want to check out this Xamarin network project, head over to the Github repo.