- vừa được xem lúc

.Net Maui Movie App Tutorial Part 1

0 0 1

Người đăng: Bab Sunny

Theo Viblo Asia

Preview App

In this tutorial I will dive into creating a similar Movie application like the one here that uses Xamarin forms but with some improvement and more features. I will however be using .Net Maui as Xamarin will be discontinued in a month or so. it will still be functional but no more fixes or future support will be provided by Microsoft meaning newer Android Os and its counterpart won't be supported for longer. Maui introduces major advantages over Xamarin such as Hot Reloads, support modern patterns and Single development project experience with . NET CLI and more. Without further ado, let's dive right in.

NOTE: This tutorial assumes you are familiar with C#, Xamarin Native, Xamarin Form app development. If you are a beginner and would like to learn the basics checkout the official documentation here for Xamarin Form then follow the tutorial i made on Xamarin Form here.

Create a Maui App on Visual Studio 2022 Open VS and create new Maui project. Can name it MauiMovieApp

Click next then select .Net 8.0 (Long term support) then create. After your enviroment is all loaded and set you can see that the Solution Explorer is different from the typical xamarin's.

There are no longer separate projects for your platforms like Android, iOS and so on. Instead we have a directory called Platforms and in here you will find Android, iOS, Windows, Tizen... directories. Another awesome feature is the ability to simply change your target build and run your application on the devices be it Android, ios or windows.

Goto themoviedb website and register to get your api key.

Next create a new directory Models and add these classes. One is MovieResponse which is the model class for the response we get from https://api.themoviedb.org/3. MovieCall is the class that formats our query and Movie is the individual model representing a single movie data. If you use the Postman to make a GET request in this format: https://api.themoviedb.org/3/movie/now_playing?page=1&api_key=yourapikey

You should get a response like example below:

"results": [ { "adult": false, "backdrop_path": "/bQS43HSLZzMjZkcHJz4fGc7fNdz.jpg", "genre_ids": [ 878, 10749, 35 ], "id": 792307, "original_language": "en", "original_title": "Poor Things", "overview": "Brought back to life by an unorthodox scientist, a young woman runs off with a debauched lawyer on a whirlwind adventure across the continents. Free from the prejudices of her times, she grows steadfast in her purpose to stand for equality and liberation.", "popularity": 425.615, "poster_path": "/kCGlIMHnOm8JPXq3rXM6c5wMxcT.jpg", "release_date": "2023-12-07", "title": "Poor Things", "video": false, "vote_average": 8.134, "vote_count": 1344 }, { "adult": false, "backdrop_path": "/a0GM57AnJtNi7lMOCamniiyV10W.jpg", "genre_ids": [ 16, 12, 14 ],......

Movie.cs

namespace MauiMovieApp.Models
{ public class Movie { public string? poster_path { get; set; } public bool adult { get; set; } public string? overview { get; set; } public string? release_date { get; set; } public List<int>? genre_ids { get; set; } public int id { get; set; } public string? original_title { get; set; } public string? original_language { get; set; } public string? title { get; set; } public string? backdrop_path { get; set; } public double popularity { get; set; } public int vote_count { get; set; } public bool video { get; set; } public double vote_average { get; set; } }
}

MovieResponse.cs

using System.Collections.ObjectModel; namespace MauiMovieApp.Models
{ class MovieResponse { public int page { get; set; } public ObservableCollection<Movie>? results { get; set; } public int total_pages { get; set; } public int total_results { get; set; } public string? status_message { get; set; } }
}

MovieCall.cs

namespace MauiMovieApp.Models
{ public class MovieCall(string type, int page, string query) { public string Type { set; get; } = type; public int Page { set; get; } = page; public string Query { set; get; } = query; }
}

Next create a directory Constants and add below classes:

ApiKeys.cs

namespace MauiMovieApp.Constants
{ class ApiKeys { public const string BASE = "https://api.themoviedb.org/3"; public const string BASE_URL = BASE + MOVIE_URL; public const string API_KEY = "insert_api_key_here"; public const string IMAGE_URL = "http://image.tmdb.org/t/p/w500"; public const string NOW_PLAYING = "now_playing"; public const string UPCOMING = "upcoming"; public const string TOP_RATED = "top_rated"; public const string POPULAR = "popular"; public const string SEARCH = BASE + SEARCH_URL; public const string MOVIE_URL = "/movie/"; public const string SEARCH_URL = "/search/movie"; }
}

Constant

namespace MauiMovieApp.Constants
{ class Constant { public const string API_FORMAT = "{0}{1}?api_key={2}&page={3}"; public const string SEARCH_FORMAT = "{0}{1}?api_key={2}&page={3}&query={4}"; public const string API_MOVIE_DETAILS_FORMAT = "{0}{1}?api_key={2}"; public const string MOVIE = "MOVIE"; public const string TITLE_NOW_PLAYING = "Now Playing"; public const string TITLE_POPULAR = "Popular"; public const string TITLE_UPCOMING = "Upcoming"; public const string TITLE_TOP_RATED = "Top Rated"; }
}

They hold the format and url extensions we will be using later in the project.

Next create a Repository directory and add 2 classes IMovieRepository and IMovieRepository

IMovieRepository

using MauiMovieApp.Models;
using System.Collections.ObjectModel; namespace MauiMovieApp.Repository
{ public interface IMovieRepository { Task<ObservableCollection<Movie>?> GetMovies(MovieCall movieCall); }
}

MovieRepository

using MauiMovieApp.Models;
using MauiMovieApp.Constants;
using Newtonsoft.Json;
using System.Collections.ObjectModel;
using System.Diagnostics; namespace MauiMovieApp.Repository
{ class MovieRepository : IMovieRepository { public async Task<ObservableCollection<Movie>?> GetMovies(MovieCall movieCall) { string url = string.Format(Constant.API_FORMAT, ApiKeys.BASE_URL, movieCall.Type, ApiKeys.API_KEY, movieCall.Page); HttpClient httpClient = new HttpClient(); try { HttpResponseMessage response = await httpClient.GetAsync(url); if (response.IsSuccessStatusCode) { string result = await response.Content.ReadAsStringAsync(); MovieResponse movieResponse = JsonConvert.DeserializeObject<MovieResponse>(result); if(movieResponse != null) { return movieResponse.results; } } } catch (Exception e){ Debug.Write(e.Message); } return null; } }
}

Note: JsonConvert will throw an error so you need to install the NewtonSoft.Json library from Manage Nugget Package for Solution in the Tools tab or use the IntelliSense suggestion.

Next create a directory ViewModels and add MoviesPageViewModel class.

MoviesPageViewModel.cs

using MauiMovieApp.Models;
using MauiMovieApp.Repository;
using MauiMovieApp.Constants;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices; namespace MauiMovieApp.Viewmodels
{ public class MoviesPageViewModel : INotifyPropertyChanged { private int page = 1; private ObservableCollection<Movie> listMovies = new ObservableCollection<Movie>(); IMovieRepository repository = DependencyService.Get<IMovieRepository>(); public Command RefreshMoviesCommand { get; set; } private bool _isRefreshing; public bool IsRefreshing { get => _isRefreshing; set => SetProperty(ref _isRefreshing, value); } private GridItemsLayout _gridItemLayout; public GridItemsLayout GridItemLayout { get => _gridItemLayout; set => SetProperty(ref _gridItemLayout, value); } public ObservableCollection<Movie> allMovies; public ObservableCollection<Movie> AllMovies { get => allMovies; set => SetProperty(ref allMovies, value); } private bool isLoadingData; public bool IsLoadingData { get => isLoadingData; set => SetProperty(ref isLoadingData, value); } public Movie _selectedMovie; public event PropertyChangedEventHandler? PropertyChanged; public Movie SelectedMovie { get { return _selectedMovie; } set { _selectedMovie = value; OpenMovieDetail(); } } public void OpenMovieDetail() { //todo } public MoviesPageViewModel() { allMovies = []; GridItemLayout = new GridItemsLayout(DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? 2 : 4, ItemsLayoutOrientation.Vertical); _ = FetchMoviesAsync(ApiKeys.NOW_PLAYING); RefreshMoviesCommand = new Command(async () => { //toQuery = ""; await FetchMoviesAsync(ApiKeys.NOW_PLAYING); IsRefreshing = false; }); } bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Object.Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public async Task FetchMoviesAsync(string type) { if (IsLoadingData) { return; } IsLoadingData = true; try { page = 1; listMovies.Clear(); allMovies.Clear(); listMovies = await repository.GetMovies(new MovieCall(type, page, "")); if (listMovies != null) { foreach (Movie movie in listMovies) { if (movie != null && !allMovies.Contains(movie)) { allMovies.Add(movie); } } } } catch (Exception e) { Console.WriteLine(e.Message); } finally { IsLoadingData = false; } } internal void SetNoOfItems(int no) { GridItemLayout.Span = no; } }
}

Let's break down all that is happening in this viewmodel class.

  • Since we aren't using Prism this time the class extends INotifyPropertyChanged to notify any changes to our items such as IsRefreshing, AllMovies and so on. Whenever we set a new value to these items the changes will be reflected on the other end. In this case our xaml where we will bind them to.

  • RefreshMoviesCommand will be used to dispose of all movie lists and refresh the page. This will be binded to the RefreshView element in the xaml layout.

  • GridItemLayout is set based on the orientation of the device. If portrait 2 else 4 items per row. This will also be updated from the xaml.cs class when orientation change is triggered so that when the user changes the orientation the layout adjusts accordingly.

  • FetchMoviesAsync is the function that makes asynchronous calls to the repository to fetch out movies. The parameter type for now is NOW_PLAYING but we will add more later in part 2.

  • Pages will be used for infinite scrolling but for now is set to 1. When we implement OnThreshold we will add this feature.

The rest is pretty straightforward. Now lets create the layout that will bind to this ViewModel. Create a new Directory Views and add a new ContentPage MoviesPage

MoviesPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="MauiMovieApp.Views.MoviesPage" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MauiMovieApp" xmlns:viewmodel="clr-namespace:MauiMovieApp.Viewmodels" Shell.NavBarIsVisible="False"> <ContentPage.BindingContext> <viewmodel:MoviesPageViewModel /> </ContentPage.BindingContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <SearchBar x:Name="searchBar" Grid.Row="0" BackgroundColor="Blue" CancelButtonColor="White" FontSize="18.0" Placeholder="Search Movies..." PlaceholderColor="White" TextColor="White" /> <RefreshView Grid.Row="1" Margin="0,0,0,0" Command="{Binding RefreshMoviesCommand}" IsRefreshing="{Binding IsRefreshing}"> <CollectionView ItemsLayout="{Binding GridItemLayout}" ItemsSource="{Binding AllMovies}" SelectedItem="{Binding SelectedMovie}" SelectionMode="Single"> <CollectionView.ItemTemplate> <DataTemplate> <Grid Margin="4"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="30" /> <RowDefinition Height="1" /> </Grid.RowDefinitions> <Image Grid.Row="0" Margin="0" Aspect="AspectFill" HeightRequest="260" WidthRequest="200"> <Image.Source> <UriImageSource CacheValidity="10:00:00:00" Uri="{Binding poster_path, StringFormat='https://image.tmdb.org/t/p/w500/{0}'}" /> </Image.Source> </Image> <Label Grid.Row="1" FontSize="14" HorizontalTextAlignment="Center" LineBreakMode="TailTruncation" Text="{Binding title}" TextColor="Black" VerticalTextAlignment="Center" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </RefreshView> <ActivityIndicator Grid.Row="1" HeightRequest="30" HorizontalOptions="Center" IsRunning="{Binding IsLoadingData}" VerticalOptions="End" WidthRequest="30" /> </Grid>
</ContentPage>

Added a SearchBar which will allow for searching movies. This will be completed in part 2. Also added a RefreshView which allows users to swipe down from the top to refresh the movies. Next a CollectionView which is binded to the AllMovies in the ViewModel class and an ActivityIndicator to show a spinner when loading data.

Next open the MoviesPage.xaml.cs and add below code.

MoviesPage.xaml.cs

using MauiMovieApp.Viewmodels; namespace MauiMovieApp.Views
{ public partial class MoviesPage : ContentPage { public MoviesPage() { InitializeComponent(); DeviceDisplay.Current.MainDisplayInfoChanged += Current_MainDisplayInfoChanged; } private void Current_MainDisplayInfoChanged(object? sender, DisplayInfoChangedEventArgs e) { var parentViewModel = (MoviesPageViewModel)this.BindingContext; parentViewModel.SetNoOfItems(DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? 2 : 4); } }
}

The Current_MainDisplayInfoChanged is triggered when an orientation change occurs. This in turn will update the viewModel SetNoOfItems. 4 items in Landscape mode and 2 in Portrait.

Open AppShell.xaml and set the Route to MoviesPage.

AppShell.xaml

<?xml version="1.0" encoding="UTF-8" ?>
<Shell x:Class="MauiMovieApp.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MauiMovieApp" xmlns:views="clr-namespace:MauiMovieApp.Views" Title="MauiMovieApp" Shell.FlyoutBehavior="Disabled"> <ShellContent Title="Movies" ContentTemplate="{DataTemplate views:MoviesPage}" Route="MoviesPage" /> </Shell>

Next register the IMovieRepository in the App.xaml.cs file.

App.xaml.cs

using MauiMovieApp.Repository; namespace MauiMovieApp { public partial class App : Application { public App() { InitializeComponent(); DependencyService.Register<IMovieRepository, MovieRepository>(); MainPage = new AppShell(); } }
}

Run the project on windows machine and you should see the application displayed as shown below if no error or mistake.

Windows Machine Demo

Next change to Android Local Device build and select your android phone that is connected to the computer. Then click start without debugging.

Android Device Potrait Demo

Android Device Landscape Demo

In the next part I will add the search feature, infinite scroll and pagination and perhaps the MovieDetails Screen. Link will be added <<here>> shortly.

Bình luận

Bài viết tương tự

- vừa được xem lúc

2 Cách để Triển Khai MVVM Trong Dự Án IOS

MVVM không nhất thiết phải bind cùng RxSwift, nhưng nó sẽ tốt hơn, vậy tại sao . MVVM Cùng Swift. Để thực hiện hai cách ràng buộc mà không phụ thuộc, chúng ta cần tạo Observable của riêng chúng ta. Đây là đoạn code :.

0 0 73

- vừa được xem lúc

2 Ways to Execute MVVM iOS

Đối với việc phát triển ứng dụng dành cho thiết bị di động, MVVM là kiến trúc hiện đại. Nó thực hiện phân tách mối quan tâm tốt hơn để làm cho mã sạch hơn.

0 0 19

- vừa được xem lúc

Một chút về MVC, MVP và MVVM

MVC, MVP, và MVVM là 3 mô hình thông dụng khi phát triển phần mềm. Trong bài viết này, mình sẽ giới thiệu với các bạn 3 mô hình Model View Controller (MVC), Model View Presenter (MVP) và Model View Vi

0 0 75

- vừa được xem lúc

Khởi tạo ViewModel sao cho hợp thời đại

Bài viết này tôi sẽ sử dụng Kotlin để khởi tạo ViewModel và AndroidViewModel. Nếu bạn chưa biết Delegation trong Kotlin thì hãy đọc bài viết này trước nhé.

0 0 58

- vừa được xem lúc

Mô hình MVVM và cách triển khai trong ứng dụng Android

Xin chào các bạn trong bài viết này, mình sẽ hướng dẫn các bạn tìm hiểu và cách triển khai Mô hình kiến trúc MVVM trong Ứng dụng Android, không khó khăn lắm đâu cùng theo dõi nha . 1>Định Nghĩa.

0 0 349

- vừa được xem lúc

BLoC Hay MVVM + GetX – Đâu là “Chân Lý” cho phát triển dự án bằng Flutter?

Trước khi đi vào tìm kiếm và so sánh giữa các Kiến trúc khi triển khai trên Flutter – Dart để xem Kiến trúc nào sẽ phù hợp, tối ưu, thuận tiện, dễ triển khai … hơn thì mình xin phép kể về hành trình v

0 0 58