Полное руководство по подключению Android к веб-API ASP.NET Core

  • Михаил
  • 12 мин. на прочтение
  • 50
  • 10 Nov 2022
  • 10 Nov 2022

В этом руководстве мы узнаем, как подключить Android к ASP.NET Core Web API. Мы создадим приложение для Android, чтобы показать список технических блогов с категориями.

С помощью Retrofit 2 приложение для Android будет подключено к RESTful API, которые мы создадим с использованием новейших технологий Microsoft и сообщества с открытым исходным кодом: ASP.NET Core Web API 5.

Наши данные будут находиться в локальной базе данных SQLServer Express, и доступ к ним будет осуществляться из нашего проекта веб-API с использованием Entity Framework Core 5.

Это полное руководство по разработке, в котором мы рассмотрим разные уровни разработки (интерфейс, серверная часть, база данных) с использованием разных языков (Java, C#, SQL) и технологий (Android, ASP.NET Core Web API, SQL Server Express). .

Итак, давайте начнем изучать, как подключить Android с помощью ASP.NET Core Web API.

Подготовка базы данных

Мы создадим нашу базу данных внутри SQL Server Express, установленного на локальном компьютере, поэтому, если он еще не установлен, загрузите и установите последние обновления  SQL Server Management Studio  и  SQL Server Express .

Как только вы сможете подключиться к SQL Server Express с помощью студии управления SQL Server, вы можете создать новую базу данных и назвать ее «BlogsDb».

После создания базы данных запустите приведенный ниже сценарий, чтобы создать необходимые таблицы и вставить в них образцы данных, чтобы мы могли начать наш учебник.

USE [BlogsDb]
GO
/****** Object:  Table [dbo].[Blog]    Script Date: 1/31/2021 5:35:24 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Blog](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[Description] [nvarchar](1000) NOT NULL,
[Url] [nvarchar](255) NOT NULL,
[RssFeed] [nvarchar](255) NULL,
[TS] [smalldatetime] NOT NULL,
[Active] [bit] NOT NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED 
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[BlogCategory]    Script Date: 1/31/2021 5:35:24 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[BlogCategory](
[BlogsId] [int] NOT NULL,
[CategoriesId] [int] NOT NULL,
CONSTRAINT [PK_BlogCategory_1] PRIMARY KEY CLUSTERED 
(
[BlogsId] ASC,
[CategoriesId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Category]    Script Date: 1/31/2021 5:35:24 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Category](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED 
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Blog] ON 
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (1, N'Coding Sonata', N'CodingSonata is the best place where you can learn new technical stuff, improve your coding skills and listen to amazing classical music', N'codingsonata.com', N'codingsonata.com/feed', CAST(N'2020-12-31T11:39:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (2, N'ASP.NET Blog', N'The official blog for ASP.NET Developers and Community', N'https://devblogs.microsoft.com/aspnet/', N'https://devblogs.microsoft.com/aspnet/feed/', CAST(N'2021-01-17T16:23:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (3, N'Android Developers Blog', N'The latest Android and Google Play news for app and game developers.', N'https://android-developers.googleblog.com/', N'https://android-developers.blogspot.com/atom.xml', CAST(N'2020-12-27T08:05:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (4, N'Google Developers', N'Engineering and technology articles for developers, written and curated by Googlers. The views expressed are those of the authors and don''t necessarily reflect those of Google.', N'https://medium.com/google-developers', N'', CAST(N'2021-01-26T10:53:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (5, N'Microsoft Azure Blog', N'Get the latest Azure news, updates, and announcements from the Azure blog. From product updates to hot topics, hear from the Azure experts.', N'https://azure.microsoft.com/en-us/blog/', N'', CAST(N'2020-12-03T12:13:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (6, N'SQL Server Blog', N'Learn how to unleash the power in your data and get the latest Microsoft SQL Server news, updates, and best practices from our Microsoft experts.', N'https://cloudblogs.microsoft.com/sqlserver/', N'', CAST(N'2021-01-27T09:20:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Blog] ([Id], [Name], [Description], [Url], [RssFeed], [TS], [Active]) VALUES (7, N'Cisco Blogs', N'Insights on Cisco''s Global Search for Innovative Technology Solutions', N'https://blogs.cisco.com/', N'https://blogs.cisco.com/feed', CAST(N'2021-01-31T19:40:00' AS SmallDateTime), 1)
GO
SET IDENTITY_INSERT [dbo].[Blog] OFF
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (1, 1)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (1, 2)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (1, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (1, 5)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (2, 1)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (2, 2)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (2, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (2, 5)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (3, 1)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (3, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (4, 1)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (4, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (5, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (5, 5)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (5, 6)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (6, 4)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (6, 5)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (7, 5)
GO
INSERT [dbo].[BlogCategory] ([BlogsId], [CategoriesId]) VALUES (7, 6)
GO
SET IDENTITY_INSERT [dbo].[Category] ON 
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (1, N'Front End Development')
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (2, N'Back End Development')
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (3, N'Desktop Development')
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (4, N'Databases')
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (5, N'DevOps and Cloud')
GO
INSERT [dbo].[Category] ([Id], [Name]) VALUES (6, N'Infrastructure and Networking')
GO
SET IDENTITY_INSERT [dbo].[Category] OFF
GO

После выполнения запроса обновите BlogsDb из обозревателя объектов, после чего вы сможете увидеть новые таблицы, созданные в базе данных BlogsDb.

 

Проект веб-API ASP.NET Core

Теперь мы перейдем к нашей части веб-API, чтобы создать RESTful API и подключиться к нашей базе данных с помощью технологии веб-API ASP.NET Core.

Создание нового проекта веб-API ASP.NET Core

Запустите Visual Studio 2019, убедитесь, что вы используете последнее обновление 16.8.x, которое включает последнюю версию .NET 5.

Выберите веб-приложение ASP.NET Core, а затем дайте ему имя, например «BlogsApi», затем выберите «Создать».

 

Затем выберите API и нажмите «Создать».

 

Подождите, пока Visual Studio подготовит для вас проект шаблона API, а затем нажмите F5 или «Выполнить». Вы должны увидеть страницу swagger в браузере по умолчанию, указывающую, что ваш проект веб-API запущен и работает нормально на вашем локальном компьютере, который является вашим IIS Express (localhost)

Теперь давайте начнем с подготовки наших сущностей и класса dbcontext, которые будут использоваться для подключения к вашей базе данных с помощью библиотеки Entity Framework Core.

Сущности

Давайте создадим новую папку с именем «Сущности» и добавим в нее класс с именем «Блог».

using System;
using System.Collections.Generic;
namespace BlogsApi.Entities
{
    public class Blog
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Url { get; set; }
        public string RssFeed { get; set; }
        public DateTime TS { get; set; }
        public bool Active { get; set; }
        public virtual ICollection<Category> Categories { get; set; }
    }
}

 

Далее добавим еще один класс с именем «Категория».

using System.Collections.Generic;
namespace BlogsApi.Entities
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Blog> Blogs { get; set; }
    }
}

Импорт EF Core

Перед добавлением DbContext нам потребуется установить пакеты nuget для EF Core и EF Core SqlServer:

Ядро Entity Framework и Entity

 

Создание DbContext

Теперь мы хотим добавить наш унаследованный класс DbContext, он унаследует класс Entity Framework Core DbContext и будет использоваться для подключения сущностей и любых других конфигураций к нашей базе данных.

Ниже приведен класс для BlogsDbContext, который будет наследоваться от DbContext EF Core:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

using Microsoft.EntityFrameworkCore;

namespace BlogsApi.Entities

{

    public class BlogsDbContext : DbContext

    {

        public DbSet<Blog> Blogs { get; set; }

        public DbSet<Category> Categories { get; set; }

        public BlogsDbContext(DbContextOptions<BlogsDbContext> options) : base(options)

        {

        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)

        {

            modelBuilder.Entity<Blog>().ToTable("Blog");

            modelBuilder.Entity<Category>().ToTable("Category");

            modelBuilder.Entity<Blog>().HasMany(s => s.Categories).WithMany(c => c.Blogs);

        }

    }

}

 

Теперь давайте подключим базу данных к коллекции сервисов. Вам нужно будет добавить приведенный ниже код внутри класса запуска в методе ConfigureServices:

1

 

services.AddDbContext<BlogsDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("BlogsDbConnectionString")));

Добавление строки подключения в appsettings.json

Откройте файл appsettings.json и добавим раздел для нашей новой строки подключения, он должен выглядеть следующим образом:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

{

  "ConnectionStrings": {

    "BlogsDbConnectionString": "Server=Home\\SQLEXPRESS;Database=BlogsDb;Trusted_Connection=True;MultipleActiveResultSets=true"

  },

  "Logging": {

    "LogLevel": {

      "Default": "Information",

      "Microsoft": "Warning",

      "Microsoft.Hosting.Lifetime": "Information"

    }

  },

  "AllowedHosts": "*"

}

 

Создание сервисов и интерфейсов

Чтобы иметь некоторое разделение в функциях и улучшить отслеживаемость и тестируемость нашего проекта веб-API, мы представим службы, которые будут действовать как бизнес-уровень и будут содержать бизнес-логику, имея возможность доступа к объекту DbContext. объект DbContext будет внедрен в используемые конструкторы служб.

Сервисы будут реализовывать интерфейсы, чтобы мы могли полагаться на абстракции, а не на конкретные реализации. Это очень важный принцип, которому нужно следовать, чтобы улучшить тестируемость и отслеживаемость кода.

Интерфейсы

Создайте новую папку с именем «Интерфейсы», затем добавьте новый элемент, вам нужно будет выбрать интерфейс и назвать его «IBlogService».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

using BlogsApi.Entities;

using System.Collections.Generic;

using System.Threading.Tasks;

namespace BlogsApi.Interfaces

{

    public interface IBlogService

    {

        Task<List<Blog>> GetAllBlogs();

        Task<List<Blog>> GetBlogsUnderCategory(int id);

    }

}

затем добавьте еще один интерфейс и назовите его «ICategoryService».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

using BlogsApi.Entities;

using System.Collections.Generic;

using System.Threading.Tasks;

namespace BlogsApi.Interfaces

{

    public interface IBlogService

    {

        Task<List<Blog>> GetAllBlogs();

        Task<List<Blog>> GetBlogsUnderCategory(int id);

    }

Услуги

Создайте новую папку с именем «Сервисы», затем добавьте класс с именем «БлогСервис».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

using BlogsApi.Entities;

using BlogsApi.Interfaces;

using Microsoft.EntityFrameworkCore;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

namespace BlogsApi.Services

{

    public class BlogService : IBlogService

    {

        private readonly BlogsDbContext blogsDbContext;

        public BlogService(BlogsDbContext blogsDbContext)

        {

            this.blogsDbContext = blogsDbContext;

        }

        public async Task<List<Blog>> GetAllBlogs()

        {

            var blogs = blogsDbContext.Blogs.Include(o => o.Categories).Where(o => o.Active).OrderByDescending(o => o.TS);

            return await blogs.ToListAsync();

        }

        public async Task<List<Blog>> GetBlogsUnderCategory(int id)

        {

            var blogs = blogsDbContext.Blogs.Include(o => o.Categories).Where(o => o.Active && o.Categories.Any(category => category.Id == id));

            return await blogs.ToListAsync();

        }

    }

}

 

И добавьте еще один класс с именем «CategoryService».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

using BlogsApi.Entities;

using BlogsApi.Interfaces;

using Microsoft.EntityFrameworkCore;

using System.Collections.Generic;

using System.Threading.Tasks;

namespace BlogsApi.Services

{

    public class CategoryService : ICategoryService

    {

        private BlogsDbContext blogsDbContext;

        public CategoryService(BlogsDbContext blogsDbContext)

        {

            this.blogsDbContext = blogsDbContext;

        }

        public async Task<List<Category>> GetCategories()

        {

            var categories = blogsDbContext.Categories;

             return await categories.ToListAsync();

        }

    }

}

 

Теперь, чтобы убедиться, что у нас есть правильная привязка между службой и интерфейсом при внедрении службы в конструктор через интерфейс, нам нужно настроить это в методе запуска ConfigureServices.

1

 

2

 

services.AddScoped<IBlogService, BlogService>();

services.AddScoped<ICategoryService, CategoryService>();

Создание моделей (DTO)

Очень важно знать, что не рекомендуется возвращать объекты сущностей как есть своим клиентам, потому что, поскольку сущности представляют точную структуру вашей базы данных, вы не хотите, чтобы ваши клиенты или (внешний мир) Если вы знаете об этой структуре, она может не представлять серьезной угрозы или проблемы, однако она может привести к некоторой утечке в структуре вашей внутренней системы и может попасть в руки вашего конкурента.

Другая причина не возвращать объект сущности состоит в том, чтобы предотвратить тесную связь с вашей внутренней структурой. Если вы собираетесь изменить какую-либо из таблиц, есть вероятность, что ваши клиенты могут быть затронуты и могут вызвать проблемы на их стороне.

Таким образом, чтобы избежать таких проблем и оставаться гибкими и слабо связанными, вы должны ввести DTO (объекты передачи данных) или модели, которые будут новыми классами, которые будут привязаны к вашим классам сущностей, которые могут включать все или часть ваших свойств сущности с тем же или разные имена или типы данных.

Итак, давайте продолжим и создадим новую папку с именем «Модели».

а затем добавьте к нему новый класс с именем BlogModel

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

using System;

using System.Collections.Generic;

namespace BlogsApi.Models

{

    public class BlogModel

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public string Url { get; set; }

        public string RssFeed { get; set; }

        public DateTime SubmittedDate { get; set; }

        public List<CategoryModel> Categories { get; set; }

    }

}

Теперь нам нужен еще один класс для CategoryModel.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

namespace BlogsApi.Models

{

    public class CategoryModel

    {

        public int Id { get; set; }

        public string Name { get; set; }

    }

}

Идеальный! Итак, что произойдет сейчас, мы преобразуем классы сущностей в классы моделей и вернем их нашим клиентам.

Обычно рекомендуется иметь вспомогательный класс для хранения логики преобразования и вызывать его только из контроллера.

Итак, прежде чем писать наши контроллеры, давайте создадим наши помощники. Создайте новую папку с именем «Помощники», а затем внутри нее создайте новый класс с именем BlogHelper.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

using BlogsApi.Entities;

using BlogsApi.Models;

using System.Collections.Generic;

using System.Linq;

namespace BlogsApi

{

    public class BlogHelper

    {

        public static List<BlogModel> ConvertBlogs(List<Blog> blogs)

        {

            var blogModels = blogs.ConvertAll(blog => new BlogModel

            {

                Id = blog.Id,

                Name = blog.Name,

                Description = blog.Description,

                Url = blog.Url,

                RssFeed = blog.RssFeed,

                SubmittedDate = blog.TS,

                Categories = blog.Categories.ToList().ConvertAll(category => new CategoryModel { Id = category.Id, Name = category.Name })

            });

        

            return blogModels;

        }

    }

}

А затем создайте еще один класс с именем CategoryHelper, он будет включать метод для преобразования сущностей категорий в модели:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

using BlogsApi.Entities;

using BlogsApi.Models;

using System.Collections.Generic;

namespace BlogsApi.Helpers

{

    public class CategoryHelper

    {

        public static List<CategoryModel> ConvertCategories(List<Category> categories)

        {

            var categoryModels = categories.ConvertAll(category => new CategoryModel

            {

                Id = category.Id,

                Name = category.Name,

            });

            return categoryModels;

        }

    }

}

Теперь мы готовы добавить наши контроллеры и склеить все компоненты вместе.

Контроллеры

Давайте создадим наши конечные точки, которые будут использоваться в качестве точек входа или шлюза в нашу базу данных для доступа к таблицам.

В папке контроллеров добавьте новый контроллер с именем BlogsController. Этот контроллер будет иметь одну конечную точку для возврата всех блогов, как вы можете видеть в методе Get(), мы вызываем метод GetAllBlogs, а затем результат передается в наш метод конвертера для преобразования типа сущностей в тип моделей, а затем результат возвращается в теле ответа http 200 ok

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

using BlogsApi.Interfaces;

using Microsoft.AspNetCore.Mvc;

using System.Threading.Tasks;

namespace BlogsApi.Controllers

{

    [ApiController]

    [Route("[controller]")]

    public class BlogsController : ControllerBase

    {

        private readonly IBlogService blogService;

        public BlogsController(IBlogService blogService)

        {

            this.blogService = blogService;

        }

        [HttpGet]

        [Route("")]

        public async Task<IActionResult> Get()

        {

            var blogs = await blogService.GetAllBlogs();

            var blogModels = BlogHelper.ConvertBlogs(blogs);

            return Ok(blogModels);

        }

    }

}

Как вы также можете заметить, мы используем внедрение конструктора BlogsController для предоставления экземпляра BlogService через абстрактный интерфейс IBlogService.

Затем давайте добавим еще один контроллер с именем «CategoriesController», он будет включать 2 конечные точки: одну для получения всех категорий, а другую для получения блогов в данной категории (id).

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

using BlogsApi.Helpers;

using BlogsApi.Interfaces;

using Microsoft.AspNetCore.Mvc;

using System.Threading.Tasks;

namespace BlogsApi.Controllers

{

    [ApiController]

    [Route("[controller]")]

    public class CategoriesController : ControllerBase

    {

        private readonly ICategoryService categoryService;

        private readonly IBlogService blogService;

        public CategoriesController(ICategoryService categoryService, IBlogService blogService)

        {

            this.categoryService = categoryService;

            this.blogService = blogService;

        }

        [HttpGet]

        [Route("")]

        public async Task<IActionResult> Get()

        {

            var categories = await categoryService.GetCategories();

            var categoryModels = CategoryHelper.ConvertCategories(categories);

            return Ok(categoryModels);

        }

        [HttpGet]

        [Route("{categoryId}/blogs")]

        public async Task<IActionResult> GetCategoryBlogs(int categoryId)

        {

            var blogs = await blogService.GetBlogsUnderCategory(categoryId);

            var blogModels = BlogHelper.ConvertBlogs(blogs);

            return Ok(blogModels);

        }

    }

}

Теперь, чтобы убедиться, что все работает нормально, нам нужно запустить проект Web API и посмотреть, какие результаты мы получим.

Здесь важно отметить, что мы не будем запускать наши API в IIS Express, мы будем запускать их на хостинге ASP.NET Core Web API по умолчанию, почему? Поскольку мы будем запускать наше Android-приложение на эмуляторе, а эмулятор должен подключаться к IP-адресу 10.0.2.2, который является другим псевдонимом для 127.0.0.1, но не локальным хостом, поэтому эмулятор не сможет подключиться к IIS Express, но он будет подключиться к узлу ASP.NET Core Web API по умолчанию.

Я покажу вам, как это будет работать позже в этом уроке.

Соответственно, из вашей Visual Studio нажмите на кнопку выпадающего списка и выберите BlogsApi вместо IIS Express.

Затем нажмите на саму кнопку BlogsApi. Это вызовет окно терминала с хостинг-провайдером ASP.NET Core Web API, загружающим ваш проект Web API.

И тогда вам должен быть представлен браузер по умолчанию, показывающий документацию Swagger по BlogsApi.

Ваш проект API теперь размещен на локальном хосте с двумя портами: 5001 https и 5000 http.

В этом руководстве мы будем подключаться к http://localhost:5000, поскольку Android требует самозаверяющий сертификат для подключения к https, поэтому это выходит за рамки данного руководства.

Помните, что в рабочей среде вы всегда должны подключать свое веб-приложение или мобильное приложение к https и создавать свои RESTful API для работы с доменами на основе https.

Конечно, мы можем протестировать наши API на Swagger с помощью удобного пользовательского интерфейса для навигации и тестирования конечных точек, однако я предпочитаю использовать Postman.

Тестирование API на Postman

Если у вас не установлен Postman, скачайте его отсюда.

Затем откройте Postman и создайте новую коллекцию с именем «BlogsApi».

Создайте новый запрос с помощью Get Categories. Это укажет на конечную точку, которая возвращает все категории

Теперь давайте создадим еще один запрос для получения всех блогов с заданным идентификатором категории.

 

Теперь последний запрос, который мы хотим протестировать, — получение всех блогов в хронологическом порядке, начиная с самого нового блога.

Итак, мы закончили создание и тестирование наших RESTful API с использованием ASP.NET Core Web API.

Конечно, это всего лишь небольшие примеры того, что вы можете делать с помощью этой мощной технологии, вы все равно можете добавлять в проект запросы POST, PUT, DELETE или даже другие GET-запросы, чтобы сделать его более крупным и всеобъемлющим.

Теперь давайте перейдем к интерфейсной части нашего руководства и подготовим наше Android-приложение для подключения к нашим RESTful API, которые мы только что создали с использованием технологии веб-API ASP.NET Core.

Создание приложения для Android

Наше Android-приложение будет отображать все категории блогов в красиво отформатированных карточках на экране запуска, который будет иметь нижнюю навигацию с 3 кнопками: категории, последние блоги и уведомления.

Всякий раз, когда пользователь нажимает на карточку, он открывает новый экран, отображающий прокручиваемый список блогов в этой конкретной категории, а затем, если пользователь нажимает на любой заданный блог, приложение отображает все сведения о блоге на новом экране с кликабельной ссылкой. чтобы открыть блог в мобильном браузере. В любое время пользователь может вернуться к предыдущим экранам с помощью кнопки возврата устройства по умолчанию.

Как упоминалось ранее в этом руководстве, мы будем подключать Android к веб-API ASP.NET Core с помощью Retrofit 2.

Итак, давайте начнем с создания нашего приложения для Android, я буду использовать Android Studio 4.1.2, если у вас нет Android Studio, вы можете скачать и установить его с официальной страницы разработчика Android , если у вас более старая версия. , я бы посоветовал вам обновить вашу версию.

Теперь откройте Android Studio и нажмите «Создать новый проект».

Затем выберите действие нижней навигации на экране шаблона проекта:

 

После этого на экране конфигурации проекта измените имя на blogs. Вы также можете изменить имя пакета на любое другое. Давайте пока оставим его как com.demo.blogs

Для минимального SDK мы выберем API 21: Android 5.0 (Lollipop) , конечно, это обычно решается на основе бизнес-требований в отношении того, какие устройства будут поддерживаться и какие API Android SDK будут использоваться.

Нажмите Finish, чтобы позволить Android Studio начать подготовку вашего проекта:

 

Давайте запустим этот пример приложения, чтобы убедиться, что эмулятор загружается и работает нормально, а приложение-шаблон нормально загружается.

Итак, как только вы увидите приведенный ниже экран в эмуляторе, это означает, что вы все настроены для начала создания приложения для блогов, которое будет подключаться к нашим RESTful API, созданным с помощью ASP.NET Core Web API.

 

Теперь закройте эмулятор и вернитесь в Android Studio.

Настройка дооснащения 2

Как упоминалось ранее в этой статье , мы будем использовать Retrofit 2 для подключения к нашему веб-API ASP.NET Core . даст вам полное пошаговое руководство по модернизации и тому, как использовать его в Android для подключения к живым API.

Итак, откройте файл build.gradle (: app) и перейдите в раздел зависимостей, добавьте приведенные ниже ссылки, чтобы получить Retrofit 2.

1

 

2

 

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

После добавления синхронизируйте свой проект, чтобы позволить Gradle загрузить и собрать проект с новыми библиотеками.

Давайте подготовим наше приложение для подключения к нашим RESTful API.

Создайте новый пакет с именем «данные» и поместите его непосредственно в пакет com.example.blogs.

Модели

Добавьте новый пакет в данные с именем «модель», он будет включать классы POJO, которые будут хранить и связывать данные с конечных точек RESTful.

Создать класс блога

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

package com.example.blogs.data.model;

public class Blog {

    private final int Id;

    private final String name;

    private final String description;

    private final String url;

    private final String rssFeed;

    private final String submittedDate;

    public Blog(int id, String name, String description, String url, String rssFeed, String submittedDate) {

        Id = id;

        this.name = name;

        this.description = description;

        this.url = url;

        this.rssFeed = rssFeed;

        this.submittedDate = submittedDate;

    }

    public int getId() {

        return Id;

    }

    public String getName() {

        return name;

    }

    public String getDescription() {

        return description;

    }

    public String getUrl() {

        return url;

    }

    public String getRssFeed() {

        return rssFeed;

    }

    public String getSubmittedDate() {

        return submittedDate;

    }

}

Затем создайте еще один класс с именем «Категория».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

package com.example.blogs.data.model;

public class Category {

    private final int id;

    private final String name;

    public Category(int id, String name) {

        this.id = id;

        this.name = name;

    }

    public int getId() {

        return id;

    }

    public String getName() {

        return name;

    }

}

Теперь добавьте новый пакет под данными с именем «remote», он будет включать классы, которые будут инициировать и подключаться к Retrofit.

Сервис и интерфейс Retrofit 2

Нам нужно будет создать интерфейс, который будет использовать аннотации из библиотеки Retrofit для сопоставления и идентификации конечных точек. Имя интерфейса — IBlogsApi, вы можете указать любое имя, которое вы предпочитаете, просто убедитесь, что оно начинается с I, чтобы придерживаться соглашений:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

package com.example.blogs.data.remote;

import com.example.blogs.data.model.Blog;

import com.example.blogs.data.model.Category;

import java.util.List;

import retrofit2.Call;

import retrofit2.http.GET;

import retrofit2.http.Path;

public interface IBlogsApi {

    @GET("categories")

    Call<List<Category>> getCategories();

    @GET("categories/{id}/blogs")

    Call<List<Blog>> getBlogsByCategory(@Path("id") int id);

    @GET("blogs")

    Call<List<Blog>> getBlogs();

}

Затем добавим класс RetrofitService, мы определим статический метод для создания экземпляра модификации.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

package com.example.blogs.data.remote;

import retrofit2.Retrofit;

import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitService {

    public static IBlogsApi Create(){

        Retrofit retrofit = new Retrofit.Builder()

                .baseUrl("http://10.0.2.2:5000/")

                .addConverterFactory(GsonConverterFactory.create())

                .build();

         return retrofit.create(IBlogsApi.class);

    }

}

Если вы заметили выше в строке baseUrl, мы подключаемся к 10.0.2.2 , как упоминалось ранее, это псевдоним интерфейса loopback хоста, который перенаправляет на 127.0.0.1 или localhost, но мы не указали localhost в baseUrl, потому что эмулятор Android может подключаться только к этому IP-адресу 10.0.2.2 с портом, указывающим 5000, где размещен веб-API ASP.NET Core.

Теперь, прежде чем перейти к следующей части вызова метода создания RetrofitService, мы расширим класс Application в новом классе MainApplication, и внутри него мы сохраним статическую ссылку на BlogsApiManager и переопределим метод onCreate приложения, чтобы получить одноэлементный экземпляр BlogsApiManager

Итак, давайте добавим новый класс непосредственно в корневой пакет corp.example.blogs.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

package com.example.blogs;

import android.app.Application;

import com.example.blogs.data.remote.BlogsApiManager;

public class MainApplication extends Application {

    public static BlogsApiManager blogsApiManager;

    @Override

    public void onCreate() {

        super.onCreate();

        blogsApiManager = BlogsApiManager.getInstance();

    }

}

БлогиApi Manager

Затем мы определим класс менеджера, который будет содержать экземпляр singleton для RetrofitService и будет включать методы, которые будут привязываться к RESTful API через события обратного вызова Retrofit:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

package com.example.blogs.data.remote;

import com.example.blogs.data.model.Blog;

import com.example.blogs.data.model.Category;

import java.util.List;

import retrofit2.Call;

import retrofit2.Callback;

public class BlogsApiManager {

    private static IBlogsApi service;

    private static BlogsApiManager apiManager;

    private BlogsApiManager() {

        service = RetrofitService.Create();

    }

    public static BlogsApiManager getInstance() {

        if (apiManager == null) {

            apiManager = new BlogsApiManager();

        }

        return apiManager;

    }

    public void getCategories(Callback<List<Category>> callback){

        Call<List<Category>> categoriesCall = service.getCategories();

        categoriesCall.enqueue(callback);

    }

    public void getBlogsByCategory(int id, Callback<List<Blog>> callback){

        Call<List<Blog>> blogsByCategoryCall = service.getBlogsByCategory(id);

        blogsByCategoryCall.enqueue(callback);

    }

    public void getBlogs(Callback<List<Blog>> callback){

        Call<List<Blog>> blogsCall = service.getBlogs();

        blogsCall.enqueue(callback);

    }

}

Репозиторий

В этом руководстве наш источник данных поступает только через удаленную службу RESTful API, которую мы создали с помощью ASP.NET Core Web API, у нас нет локального источника данных для подключения, поэтому уровень репозитория будет включать только вызовы BlogsApiManager и будет хранить данные в объектах LiveData, которые позже будут переданы на уровень пользовательского интерфейса, чтобы просмотреть ModelView конкретного компонента пользовательского интерфейса.

Эта структура многоуровневого обслуживания подпадает под шаблон архитектурного проектирования MVVM с использованием компонентов LiveData и ModelView Android X.

Итак, давайте добавим новый пакет под данными с именем «репозиторий», он будет включать в себя репозиторий нашего приложения для блогов.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

52

 

53

 

54

 

55

 

56

 

57

 

58

 

59

 

60

 

61

 

62

 

63

 

64

 

65

 

66

 

67

 

68

 

69

 

70

 

71

 

72

 

73

 

74

 

75

 

76

 

77

 

78

 

79

 

80

 

81

 

82

 

83

 

84

 

85

 

86

 

87

 

88

 

89

 

90

 

91

 

92

 

93

 

94

 

95

 

package com.example.blogs.data.repository;

import androidx.lifecycle.MutableLiveData;

import com.example.blogs.data.model.Blog;

import com.example.blogs.data.remote.BlogsApiManager;

import com.example.blogs.data.model.Category;

import java.util.List;

import retrofit2.Call;

import retrofit2.Callback;

import retrofit2.Response;

public class BlogsRepository {

    private static volatile BlogsRepository instance;

    private final BlogsApiManager blogsApiManager;

    private final MutableLiveData<List<Category>> categories = new MutableLiveData<>();

    private final MutableLiveData<List<Blog>> blogsByCategory = new MutableLiveData<>();

    private final MutableLiveData<List<Blog>> blogs = new MutableLiveData<>();

    private BlogsRepository(BlogsApiManager blogsApiManager) {

        this.blogsApiManager = blogsApiManager;

    }

    public static BlogsRepository getInstance(BlogsApiManager blogsApiManager) {

        if (instance == null) {

            instance = new BlogsRepository(blogsApiManager);

        }

        return instance;

    }

    public MutableLiveData<List<Category>> getCategories(){

        blogsApiManager.getCategories(new Callback<List<Category>>() {

            @Override

            public void onResponse(Call<List<Category>> call, Response<List<Category>> response) {

                if (response.isSuccessful()){

                    List<Category> body = response.body();

                    categories.setValue(body);

                } else{

                    categories.postValue(null);

                }

            }

            @Override

            public void onFailure(Call<List<Category>> call, Throwable t) {

                categories.postValue(null);

            }

        });

        return categories;

    }

    public MutableLiveData<List<Blog>> getBlogsByCategory(int id){

        blogsApiManager.getBlogsByCategory(id, new Callback<List<Blog>>() {

            @Override

            public void onResponse(Call<List<Blog>> call, Response<List<Blog>> response) {

                if (response.isSuccessful()){

                    List<Blog> body = response.body();

                    blogsByCategory.setValue(body);

                } else{

                    blogsByCategory.postValue(null);

                }

            }

            @Override

            public void onFailure(Call<List<Blog>> call, Throwable t) {

                blogsByCategory.postValue(null);

            }

        });

        return blogsByCategory;

    }

    public MutableLiveData<List<Blog>> getBlogs(){

        blogsApiManager.getBlogs(new Callback<List<Blog>>() {

            @Override

            public void onResponse(Call<List<Blog>> call, Response<List<Blog>> response) {

                if (response.isSuccessful()){

                    List<Blog> body = response.body();

                    blogs.setValue(body);

                } else{

                    blogs.postValue(null);

                }

            }

            @Override

            public void onFailure(Call<List<Blog>> call, Throwable t) {

                blogs.postValue(null);

            }

        });

        return blogs;

    }

}

Теперь давайте подготовим часть пользовательского интерфейса приложения.

интерфейс

Разверните пакет ui в проводнике вашего проекта, вы увидите 3 пакета, созданных для вас по шаблону, который мы выбрали ранее.

также вы заметите, что MainActivity создана для вас, это действие хостинга для фрагмента узла навигации, который будет использовать BottomNavigationView для переключения между различными фрагментами.

Это код для MainActivity, вам не нужно делать с ним ничего конкретного.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

package com.example.blogs.ui;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import androidx.navigation.NavController;

import androidx.navigation.Navigation;

import androidx.navigation.ui.AppBarConfiguration;

import androidx.navigation.ui.NavigationUI;

import com.example.blogs.R;

import com.google.android.material.bottomnavigation.BottomNavigationView;

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        BottomNavigationView navView = findViewById(R.id.nav_view);

        // Passing each menu ID as a set of Ids because each

        // menu should be considered as top level destinations.

        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(

                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)

                .build();

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);

        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        NavigationUI.setupWithNavController(navView, navController);

    }

}

 

Нам нужно будет переименовать домашний пакет в категории, поэтому щелкните правой кнопкой мыши домашний пакет и выполните рефакторинг -> переименовать (или просто используйте Shift + F6 на клавиатуре) и используйте имя «категории».

Пакет категорий

Прежде чем мы перейдем к части фрагмента раздела категорий, мы создадим для нее ViewModel, которая будет действовать как промежуточный слой между пользовательским интерфейсом и моделью.

Итак, добавьте новый класс с CategoriesViewModel.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

package com.example.blogs.ui.categories;

import androidx.lifecycle.MutableLiveData;

import androidx.lifecycle.ViewModel;

import com.example.blogs.data.model.Category;

import com.example.blogs.data.repository.BlogsRepository;

import java.util.List;

public class CategoriesViewModel extends ViewModel {

    private final BlogsRepository categoryRepository;

    public CategoriesViewModel(BlogsRepository categoryRepository) {

        this.categoryRepository = categoryRepository;

    }

    public MutableLiveData<List<Category>> getCategories() {

        return categoryRepository.getCategories();

    }

}

А теперь давайте добавим фабричный класс CategoriesViewModelFactory, который создаст экземпляр ViewModel во фрагменте.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

package com.example.blogs.ui.categories;

import androidx.annotation.NonNull;

import androidx.lifecycle.ViewModel;

import androidx.lifecycle.ViewModelProvider;

import com.example.blogs.MainApplication;

import com.example.blogs.data.repository.BlogsRepository;

public class CategoriesViewModelFactory implements ViewModelProvider.Factory {

    @NonNull

    @Override

    @SuppressWarnings("unchecked")

    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {

        if (modelClass.isAssignableFrom(CategoriesViewModel.class)) {

            return (T) new CategoriesViewModel(BlogsRepository.getInstance(MainApplication.blogsApiManager));

        } else {

            throw new IllegalArgumentException("Unknown ViewModel class");

        }

    }

}

Мы хотим отображать категории через recyclerview с GridLayoutManager, имеющим 2 категории в строке, и каждая категория будет отображаться в представлении карты.

Итак, давайте начнем с подготовки макета элемента категории, который будет использоваться для отображения карточки категории.

Макет элемента категории

перейдите в папку res/layout и добавьте новый файл ресурсов макета с именем category_item.xml

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

<?xml version="1.0" encoding="utf-8"?>

<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:cardView="http://schemas.android.com/apk/res-auto"

    android:id="@+id/carView"

    android:layout_width="match_parent"

    android:layout_height="100dp"

    cardView:cardCornerRadius="5dp"

    cardView:cardElevation="5dp"

    android:layout_margin="5dp">

    <TextView

        android:id="@+id/category_name"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="14dp"

        android:text="Sample"

        android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />

</androidx.cardview.widget.CardView>

Категории Адаптер

Теперь давайте создадим адаптер, который будет привязывать данные к recyclerview, используя макет card_item.

Внутри пакета категорий создайте новый класс с именем «CategoriesAdapter».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

52

 

53

 

54

 

55

 

56

 

57

 

58

 

59

 

60

 

61

 

62

 

63

 

64

 

65

 

66

 

67

 

68

 

package com.example.blogs.ui.categories;

import android.content.Context;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.TextView;

import androidx.annotation.NonNull;

import androidx.recyclerview.widget.RecyclerView;

import com.example.blogs.R;

import com.example.blogs.data.model.Category;

import com.example.blogs.ui.common.OnItemClickListener;

import java.util.ArrayList;

import java.util.List;

public class CategoriesAdapter extends RecyclerView.Adapter<CategoriesAdapter.CategoryViewHolder> {

    @NonNull

    private final Context context;

    private List<Category> categories = new ArrayList<>();

    private final OnItemClickListener<Category> onCategoryClickListener;

    public CategoriesAdapter(@NonNull Context context, OnItemClickListener<Category> onCategoryClickListener) {

        this.context = context;

        this.onCategoryClickListener = onCategoryClickListener;

    }

    @Override

    public CategoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        return new CategoryViewHolder(LayoutInflater.from(context).inflate(R.layout.category_item, parent, false));

    }

    @Override

    public void onBindViewHolder(CategoryViewHolder holder, int position) {

        holder.setCategoryItem(categories.get(position));

    }

    @Override

    public int getItemCount() {

        return categories == null ? 0 : categories.size();

    }

    public void setCategories(List<Category> categories) {

        this.categories = categories;

        this.notifyDataSetChanged();

    }

    class CategoryViewHolder extends RecyclerView.ViewHolder {

        private final TextView categoryName;

        private final View categoryItem;

        CategoryViewHolder(View categoryItem) {

            super(categoryItem);

            categoryName = categoryItem.findViewById(R.id.category_name);

            this.categoryItem = categoryItem;

        }

        private void setCategoryItem(Category category){

            categoryName.setText(category.getName());

            categoryItem.setOnClickListener(view -> onCategoryClickListener.onItemClicked(view, category));

        }

    }

}

OnItemClickListener

В приведенном выше коде вы заметите

1

 

import com.example.blogs.ui.common.OnItemClickListener;

мы добавили прослушиватель кликов для наших элементов карточек категорий, чтобы всякий раз, когда пользователь нажимает на категорию, мы могли знать, какая категория была выбрана, и, таким образом, переходить на новый экран с блогами, перечисленными в выбранной категории.

Итак, откройте пакет пользовательского интерфейса и создайте новый пакет с именем «common», а внутри него добавьте интерфейс с именем «OnItemClickListener».

1

 

2

 

3

 

4

 

5

 

6

 

7

 

package com.example.blogs.ui.common;

import android.view.View;

public interface OnItemClickListener<T> {

    void onItemClicked(View view, T data);

}

Категории Фрагмент

Теперь перейдите к HomeFragment и переименуйте его в CategoriesFragment. Это будет содержать весь код, связанный с пользовательским интерфейсом, для обновления представления, он будет наблюдать за CategoriesViewModel для любых изменений, а затем соответствующим образом обновлять recyclerview и его адаптер, мы также будем отображать прогресс во время вызова API и скрывать его после получения результатов. .

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

52

 

53

 

54

 

package com.example.blogs.ui.categories;

import android.content.Intent;

import android.os.Bundle;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import androidx.annotation.NonNull;

import androidx.core.widget.ContentLoadingProgressBar;

import androidx.fragment.app.Fragment;

import androidx.lifecycle.ViewModelProvider;

import androidx.recyclerview.widget.GridLayoutManager;

import androidx.recyclerview.widget.RecyclerView;

import com.example.blogs.R;

import com.example.blogs.data.model.Category;

import com.example.blogs.ui.blogs.BlogsActivity;

import com.example.blogs.ui.common.OnItemClickListener;

import com.google.gson.Gson;

public class CategoriesFragment extends Fragment {

    private CategoriesAdapter categoryAdapter;

    public View onCreateView(@NonNull LayoutInflater inflater,

                             ViewGroup container, Bundle savedInstanceState) {

        CategoriesViewModel homeViewModel = new ViewModelProvider(this, new CategoriesViewModelFactory()).get(CategoriesViewModel.class);

        View root = inflater.inflate(R.layout.fragment_categories, container, false);

        ContentLoadingProgressBar progress = root.findViewById(R.id.progress);

        RecyclerView categoriesRecyclerView = root.findViewById(R.id.categories_recycler_view);

        OnItemClickListener<Category> onCategoryClickListener = (view, category) -> {

            Intent intent = new Intent(getActivity(), BlogsActivity.class);

            String categoryJson = new Gson().toJson(category);

            intent.putExtra("Category", categoryJson);

            intent.putExtra("CallerActivity", getActivity().getClass().getSimpleName());

            startActivity(intent);

        };

        categoryAdapter = new CategoriesAdapter(root.getContext(), onCategoryClickListener);

        categoriesRecyclerView.setAdapter(categoryAdapter);

        categoriesRecyclerView.setLayoutManager(new GridLayoutManager(root.getContext(), 2));

        progress.show();

        homeViewModel.getCategories().observe(getViewLifecycleOwner(), categories -> {

            categoryAdapter.setCategories(categories);

            progress.hide();

        });

        return root;

    }

}

Теперь создадим макет fragment_categories.xml:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context=".ui.categories.CategoriesFragment">

    <androidx.core.widget.ContentLoadingProgressBar

        android:id="@+id/progress"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        style="?android:attr/progressBarStyleLarge"

        android:visibility="visible"

        android:indeterminateTint="@color/purple_700"

        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintStart_toStartOf="parent" />

    <androidx.recyclerview.widget.RecyclerView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:id="@+id/categories_recycler_view"

        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintStart_toStartOf="parent"

        app:layout_constraintTop_toTopOf="parent"

        />

</androidx.constraintlayout.widget.ConstraintLayout>

 

БлогиАктивность

Теперь мы закончили с частью «Фрагмент категорий», пришло время создать новую активность, которая будет отображать все блоги в выбранной категории.

BlogsActivity будет вызываться из CategoriesFragment и получит намерение с объектом категории, проанализированным как json, и CallerActivity, чтобы BlogsActivity отображал все свои блоги внутри фрагмента, который будет использоваться совместно и использоваться во втором макете нижней навигации. последние блоги

Итак, в пакете ui создайте новый пакет с именем «блоги», внутри него создайте новое действие, выберите пустое действие.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

package com.example.blogs.ui.blogs;

import android.content.Intent;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import androidx.appcompat.widget.Toolbar;

import androidx.fragment.app.FragmentTransaction;

import com.example.blogs.R;

import com.example.blogs.data.model.Category;

import com.google.gson.Gson;

public class BlogsActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_blogs);

        Toolbar toolbar = findViewById(R.id.toolbar);

        setSupportActionBar(toolbar);

        Intent intent = getIntent();

        String categoryJson = intent.getStringExtra("Category");

        Category category = new Gson().fromJson(categoryJson, Category.class);

        setTitle(category.getName());

        String callerActivity = intent.getStringExtra("CallerActivity");

        BlogsFragment fragment = new BlogsFragment();

        Bundle args = new Bundle();

        args.putInt("CategoryId", category.getId());

        args.putString("CallerActivity", callerActivity);

        if (savedInstanceState == null){

            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();

            fragment.setArguments(args);

            ft.replace(R.id.blogs_fragment_container, fragment);

            ft.commit();

        }

    }

}

Давайте добавим макет xml для activity_blogs, он будет содержать FragmentContainerView для размещения фрагмента и AppBarLayout для отображения верхней панели с названием категории, как вы можете видеть из предыдущего кода, мы устанавливаем заголовок активности для выбранного имени категории , после прочтения намерения и преобразования его в объект категории из строкового формата Json.

Ниже приведен файл activity_blogs.xml.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

<?xml version="1.0" encoding="utf-8"?>

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context=".ui.blogs.BlogsActivity">

    <com.google.android.material.appbar.AppBarLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:id="@+id/app_bar"

        android:theme="@style/Theme.Blogs.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar

            android:id="@+id/toolbar"

            android:layout_width="match_parent"

            android:layout_height="?attr/actionBarSize"

            android:background="?attr/colorPrimary"

            app:popupTheme="@style/Theme.Blogs.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.fragment.app.FragmentContainerView

        android:id="@+id/blogs_fragment_container"

        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"

        android:name="androidx.navigation.fragment.NavHostFragment"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        app:defaultNavHost="true"

        app:navGraph="@navigation/mobile_navigation" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

БлогиФрагмент

Теперь давайте создадим BlogsFragment. Как упоминалось ранее, этот фрагмент будет использоваться из 2 разных мест, поэтому он разветвит свои аргументы для проверки CallerActivity, если он исходит из MainActivity, то это означает, что этот фрагмент будет показывать блоги выбранной категории, потому что мы передаем CallerActivity прямо из CategoriesFragment, который размещен в последних блогах MainActivity, поэтому он вызовет метод getBlogs, иначе он покажет последние блоги.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

52

 

53

 

54

 

55

 

56

 

57

 

58

 

59

 

60

 

61

 

62

 

63

 

64

 

65

 

66

 

67

 

68

 

69

 

70

 

71

 

72

 

73

 

74

 

75

 

76

 

77

 

78

 

79

 

80

 

81

 

82

 

83

 

84

 

85

 

86

 

87

 

package com.example.blogs.ui.blogs;

import android.content.Intent;

import android.os.Bundle;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.LinearLayout;

import androidx.annotation.NonNull;

import androidx.core.widget.ContentLoadingProgressBar;

import androidx.fragment.app.Fragment;

import androidx.lifecycle.Observer;

import androidx.lifecycle.ViewModelProvider;

import androidx.recyclerview.widget.DividerItemDecoration;

import androidx.recyclerview.widget.LinearLayoutManager;

import androidx.recyclerview.widget.RecyclerView;

import com.example.blogs.R;

import com.example.blogs.data.model.Blog;

import com.example.blogs.ui.MainActivity;

import com.example.blogs.ui.blog.info.BlogInfoActivity;

import com.example.blogs.ui.common.OnItemClickListener;

import com.google.android.material.snackbar.BaseTransientBottomBar;

import com.google.android.material.snackbar.Snackbar;

import com.google.gson.Gson;

import java.util.ArrayList;

import java.util.List;

public class BlogsFragment extends Fragment {

    private BlogsAdapter blogsAdapter;

    public View onCreateView(@NonNull LayoutInflater inflater,

                             ViewGroup container, Bundle savedInstanceState) {

        BlogsViewModel blogViewModel = new ViewModelProvider(this, new BlogsViewModelFactory()).get(BlogsViewModel.class);

        View root = inflater.inflate(R.layout.fragment_blogs, container, false);

        ContentLoadingProgressBar progress = root.findViewById(R.id.progress);

        Bundle arguments = this.getArguments();

        String callerActivity = "";

        int categoryId = 0;

        if (arguments != null){

            callerActivity = arguments.getString("CallerActivity");

            categoryId = arguments.getInt("CategoryId");

        }

        RecyclerView blogsRecyclerView = root.findViewById(R.id.blogs_recycler_view);

        OnItemClickListener<Blog> onBlogClickListener = (view, blog) -> {

            Gson gson = new Gson();

            String blogJson = gson.toJson(blog);

            Intent intent = new Intent(getActivity(), BlogInfoActivity.class);

            intent.putExtra("Blog", blogJson);

            intent.putExtra("CallerActivity", getActivity().getClass().getSimpleName());

            startActivity(intent);

        };

        blogsAdapter = new BlogsAdapter(root.getContext(), onBlogClickListener);

        blogsRecyclerView.addItemDecoration(new DividerItemDecoration(root.getContext(), LinearLayout.VERTICAL));

        blogsRecyclerView.setAdapter(blogsAdapter);

        blogsRecyclerView.setLayoutManager(new LinearLayoutManager(root.getContext()));

        Snackbar make = Snackbar.make(getActivity().findViewById(android.R.id.content), "No blogs found for this category", BaseTransientBottomBar.LENGTH_INDEFINITE);

        Observer<List<Blog>> blogsObserver = blogs -> {

            if (blogs == null || blogs.size() == 0) {

                make.show();

                blogsAdapter.setBlogs(new ArrayList<>());

            } else {

                make.dismiss();

                blogsAdapter.setBlogs(blogs);

            }

            progress.hide();

        };

        progress.show();

        if (callerActivity.equals(MainActivity.class.getSimpleName())){

            blogViewModel.getBlogsByCategory(categoryId).observe(getViewLifecycleOwner(), blogsObserver);

        } else {

            blogViewModel.getBlogs().observe(getViewLifecycleOwner(), blogsObserver);

        }

        return root;

    }

}

И давайте изучим XML-макет фрагмента_блога после того, как вы создадите его в папке res/layout:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <androidx.core.widget.ContentLoadingProgressBar

        android:id="@+id/progress"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        style="?android:attr/progressBarStyleLarge"

        android:visibility="visible"

        android:indeterminateTint="@color/purple_700"

        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintEnd_toEndOf="parent"

        app:layout_constraintStart_toStartOf="parent" />

    <androidx.recyclerview.widget.RecyclerView

        android:id="@+id/blogs_recycler_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

фрагмент блогов включает в себя recyclerview для отображения списка блогов и имеет ContentLoadingProgressBar для отображения удобной загрузки счетчика для пользователя, пока приложение выполняет запрос.

БлогиViewModel

BlogsViewModel будет включать 2 метода из BlogsRepository: один для получения блогов в выбранной категории, а другой для получения последних категорий, упорядоченных по дате.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

package com.example.blogs.ui.blogs;

import androidx.lifecycle.MutableLiveData;

import androidx.lifecycle.ViewModel;

import com.example.blogs.data.model.Blog;

import com.example.blogs.data.model.Category;

import com.example.blogs.data.repository.BlogsRepository;

import java.util.List;

public class BlogsViewModel extends ViewModel {

    private MutableLiveData<List<Blog>> blogs;

    private BlogsRepository blogsRepository;

    public BlogsViewModel(BlogsRepository blogsRepository) {

        this.blogsRepository = blogsRepository;

    }

    public MutableLiveData<List<Blog>> getBlogsByCategory(int id) {

        blogs = blogsRepository.getBlogsByCategory(id);

        return blogs;

    }

    public MutableLiveData<List<Blog>> getBlogs() {

        blogs = blogsRepository.getBlogs();

        return blogs;

    }

}

БлогиViewModelFactory

И вот BlogsViewModelFactory, он похож на CategoriesViewModelFactory с точки зрения использования BlogsRepository для получения одноэлементного экземпляра BlogsApiManager.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

package com.example.blogs.ui.blogs;

import androidx.annotation.NonNull;

import androidx.lifecycle.ViewModel;

import androidx.lifecycle.ViewModelProvider;

import com.example.blogs.MainApplication;

import com.example.blogs.data.repository.BlogsRepository;

public class BlogsViewModelFactory implements ViewModelProvider.Factory {

    @NonNull

    @Override

    @SuppressWarnings("unchecked")

    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {

        if (modelClass.isAssignableFrom(BlogsViewModel.class)) {

            return (T) new BlogsViewModel(BlogsRepository.getInstance(MainApplication.blogsApiManager));

        } else {

            throw new IllegalArgumentException("Unknown ViewModel class");

        }

    }

}

БлогиАдаптер

А теперь мы создадим BlogsAdapter, он будет использоваться для привязки blog_item к источнику данных блогов, переданному из фрагмента

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

52

 

53

 

54

 

55

 

56

 

57

 

58

 

59

 

60

 

61

 

62

 

63

 

64

 

65

 

66

 

67

 

68

 

69

 

70

 

71

 

72

 

73

 

74

 

75

 

76

 

77

 

78

 

79

 

80

 

81

 

82

 

package com.example.blogs.ui.blogs;

import android.content.Context;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.TextView;

import androidx.annotation.NonNull;

import androidx.recyclerview.widget.RecyclerView;

import com.example.blogs.R;

import com.example.blogs.data.helper.DateHelper;

import com.example.blogs.data.model.Blog;

import com.example.blogs.ui.common.OnItemClickListener;

import java.text.ParseException;

import java.util.ArrayList;

import java.util.List;

public class BlogsAdapter extends RecyclerView.Adapter<BlogsAdapter.BlogViewHolder> {

    @NonNull

    private final Context context;

    private List<Blog> blogs = new ArrayList<>();

    private final OnItemClickListener<Blog> onBlogItemClickListener;

    public BlogsAdapter(@NonNull Context context, OnItemClickListener<Blog> onBlogItemClickListener) {

        this.context = context;

        this.onBlogItemClickListener = onBlogItemClickListener;

    }

    @Override

    public BlogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        return new BlogViewHolder(LayoutInflater.from(context).inflate(R.layout.blog_item, parent, false));

    }

    @Override

    public void onBindViewHolder(BlogViewHolder holder, int position) {

        try {

            holder.setBlogItem(blogs.get(position));

        } catch (ParseException e) {

            e.printStackTrace();

        }

    }

    @Override

    public int getItemCount() {

        return blogs == null ? 0 : blogs.size();

    }

    public void setBlogs(List<Blog> blogs) {

        this.blogs = blogs;

        this.notifyDataSetChanged();

    }

    class BlogViewHolder extends RecyclerView.ViewHolder {

        private final TextView blogName;

        private final TextView blogDescription;

        private final TextView blogDate;

        private final View regionItem;

        BlogViewHolder(View regionItem) {

            super(regionItem);

            this.regionItem = regionItem;

            blogName = regionItem.findViewById(R.id.blog_name);

            blogDescription = regionItem.findViewById(R.id.blog_description);

            blogDate = regionItem.findViewById(R.id.blog_date);

        }

        private void setBlogItem(Blog blog) throws ParseException {

            regionItem.setOnClickListener(view -> onBlogItemClickListener.onItemClicked(view, blog));

            blogName.setText(blog.getName());

            blogDescription.setText(blog.getDescription());

            String formattedDate = DateHelper.getFormattedDate(blog.getSubmittedDate());

            blogDate.setText(formattedDate);

        }

    }

}

И давайте посмотрим, как будет выглядеть макет blog_item.xml, как показано ниже:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:padding="10dp"

    android:layout_width="match_parent"

    android:layout_height="wrap_content">

    <TextView

        android:id="@+id/blog_name"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="10dp"

        android:textStyle="bold"

        android:text="Blog Name"

        android:textAppearance="@style/TextAppearance.AppCompat.Medium" />

    <TextView

        android:id="@+id/blog_description"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="8dp"

        android:text="This is a Sample Blog description with one line only displayed"

        android:ellipsize="end"

        android:maxLines="2"

        android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />

    <TextView

        android:id="@+id/blog_date"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="8dp"

        android:textAlignment="textEnd"

        android:text="-"

        android:ellipsize="end"

        android:maxLines="2"

        android:textSize="12sp"

        android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />

</LinearLayout>

До сих пор мы реализовали экраны категорий и блогов, теперь давайте реализуем экран, который будет получать сведения о блоге и отображать их для пользователя.

БлогиИнформацияАктивность

под пакетом ui добавьте новый пакет с именем blog.info и внутри него создайте новую пустую активность и назовите ее BlogInfoActivity, внутри этой активности мы будем отображать всю информацию о блоге, для этого не нужно создавать фрагмент, потому что в В этом уроке у нас не будет другого экрана или раздела для отображения информации о блоге.

BlogsInfoActivity должен иметь следующий код:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

40

 

41

 

42

 

43

 

44

 

45

 

46

 

47

 

48

 

49

 

50

 

51

 

package com.example.blogs.ui.blog.info;

import android.content.Intent;

import android.os.Bundle;

import com.example.blogs.data.helper.DateHelper;

import com.example.blogs.data.model.Blog;

import androidx.appcompat.app.AppCompatActivity;

import androidx.appcompat.widget.Toolbar;

import android.widget.TextView;

import android.widget.Toast;

import com.example.blogs.R;

import com.google.gson.Gson;

public class BlogInfoActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_blog_info);

        Toolbar toolbar = findViewById(R.id.toolbar);

        setSupportActionBar(toolbar);

        Intent intent = getIntent();

        String blogJson = intent.getStringExtra("Blog");

        Blog blog = new Gson().fromJson(blogJson, Blog.class);

        if (blog == null){

            Toast.makeText(this, "Invalid blog", Toast.LENGTH_LONG).show();

            return;

        }

        TextView blogName = findViewById(R.id.blog_name);

        TextView blogDescription = findViewById(R.id.blog_description);

        TextView blogUrl = findViewById(R.id.blog_url);

        TextView blogRss = findViewById(R.id.blog_rss);

        TextView blogDate = findViewById(R.id.blog_date);

        blogName.setText(blog.getName());

        blogDescription.setText(blog.getDescription());

        blogUrl.setText(blog.getUrl());

        blogRss.setText(blog.getRssFeed());

        blogDate.setText(DateHelper.getFormattedDate(blog.getSubmittedDate()));

    }

}

Если вы заметили из последней строки в приведенном выше исходном коде, мы добавили метод для форматирования даты

1

 

blogDate.setText(DateHelper.getFormattedDate(blog.getSubmittedDate()));

getFormattedDate принимает строку, содержащую дату блога в формате UTC, и форматирует ее для отображения в более презентабельном формате даты и времени.

давайте перейдем к пакету данных и создадим новый пакет с именем helper, а внутри него добавим новый класс с именем DateHelper.

 

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

package com.example.blogs.data.helper;

import java.text.DateFormat;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.Locale;

public class DateHelper {

    public static String getFormattedDate(String date)  {

        SimpleDateFormat displayDateFormat = new SimpleDateFormat("MMM dd, yyy h:mm a", Locale.US);

        DateFormat inputDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);

        Date formattedDate = null;

        try {

            formattedDate = inputDateFormat.parse(date);

        } catch (ParseException e) {

            e.printStackTrace();

        }

        if (formattedDate == null){

            return "-";

        }

        return displayDateFormat.format(formattedDate);

    }

}

поэтому давайте вернемся в папку res/layout и создадим новый ресурс макета с именем «activity_blog_info.xml»

Это источник для activity_blog_info.xml , вы можете свободно оформлять его по своему усмотрению:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

35

 

36

 

37

 

38

 

39

 

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:padding="10dp"

    android:layout_width="match_parent"

    android:layout_height="wrap_content">

    <TextView

        android:id="@+id/blog_name"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="10dp"

        android:textStyle="bold"

        android:text="Blog Name"

        android:textAppearance="@style/TextAppearance.AppCompat.Medium" />

    <TextView

        android:id="@+id/blog_description"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="8dp"

        android:text="This is a Sample Blog description with one line only displayed"

        android:ellipsize="end"

        android:maxLines="2"

        android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />

    <TextView

        android:id="@+id/blog_date"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:padding="8dp"

        android:textAlignment="textEnd"

        android:text="-"

        android:ellipsize="end"

        android:maxLines="2"

        android:textSize="12sp"

        android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" />

</LinearLayout>

Добавление конфигурации сети

Перед тестированием нашей работы на эмуляторе нам просто нужно сделать последний шаг, мы добавим новый пакет с именем xml в папку res, а внутри него мы добавим файл xml с именем network_security_config.

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

<?xml version="1.0" encoding="utf-8"?>

<!-- IMPORTANT NOTE:

the configuration setting cleartextTrafficPermitted=\"true\" should only be used for testing purposes,

when connecting to APIs on production you should always connect to https based endpoints instead of http

-->

<network-security-config>

    <base-config cleartextTrafficPermitted="true">

        <trust-anchors>

            <certificates src="system" />

        </trust-anchors>

    </base-config>

</network-security-config>

Я добавил важное примечание, имейте это в виду, что вы не должны использовать cleartextTrafficPermitted="true" в производственной среде, что означает, что вы всегда должны подключаться к https API при работе с бизнес-продуктами. Это добавлено только потому, что мы тестируем наше приложение на эмуляторе, который подключен к локальному хосту через специальный IP-адрес (10.0.2.2), поэтому мы подключаемся к нашим API через HTTP-вызовы.

Давайте включим указанный выше network_security_config в манифест нашего приложения, добавим строку ниже в тег <application

1

 

android:networkSecurityConfig="@xml/network_security_config"

И поскольку мы все еще находимся в манифесте, убедитесь, что вы указали MainActivity в качестве основного действия и действия запуска.

В итоге ваш манифест должен выглядеть так:

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 

24

 

25

 

26

 

27

 

28

 

29

 

30

 

31

 

32

 

33

 

34

 

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.example.blogs">

    <uses-permission android:name="android.permission.INTERNET" />

    <application

        android:name=".MainApplication"

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:networkSecurityConfig="@xml/network_security_config"

        android:roundIcon="@mipmap/ic_launcher_round"

        android:supportsRtl="true"

        android:theme="@style/Theme.Blogs">

        <activity

            android:name=".ui.blog.info.BlogInfoActivity"

            android:label="@string/title_activity_blog_info"

            android:theme="@style/Theme.Blogs.NoActionBar" />

        <activity

            android:name=".ui.blogs.BlogsActivity"

            android:label="@string/title_activity_blog"

            android:theme="@style/Theme.Blogs.NoActionBar" />

        <activity

            android:name=".ui.MainActivity"

            android:label="@string/title_activity_main">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>

</manifest>

И ваша структура папок Android должна выглядеть так

Структура папок приложения Android Blogs

 

Тестирование Android-приложения на эмуляторе

Теперь давайте запустим наше приложение, чтобы посмотреть, как оно будет выглядеть в эмуляторе.

Первый экран (экран запуска) — это MainActivity, а первый отображаемый фрагмент — это фрагмент категорий, поэтому мы увидим категории, заполненные карточками через макет сетки.

 

Теперь, если мы нажмем на Front End Development, мы увидим новый экран с блогами, расположенными вертикально на экране, и заголовок будет Front End Development.

 

Затем, если мы нажмем на Coding Sonata, мы увидим другой экран со всеми подробностями блога, относящимися к выбранному блогу.

Теперь давайте дважды вернемся к главному экрану, на котором показаны категории, и выберите разработку для настольных компьютеров. Мы не добавляли блоги в эту категорию, поэтому на следующем экране мы увидим панель снэк-бара, сообщающую нам, что в этой категории нет блогов.

Теперь давайте вернемся на главный экран и коснемся кнопки последних блогов, вы должны увидеть последние блоги, отсортированные по дате.

 

и вот как счетчик прогресса загрузки будет отображаться всякий раз, когда происходит вызов API для получения категорий

 

Резюме

Вот и все, нам удалось создать и подключить Android к веб-API ASP.NET Core в .NET 5, мы использовали Entity Framework Core 5 для подключения к базе данных SQL Server Express. Приложение для Android, предназначенное для Android SDK 30 с минимальным пакетом SDK 21, подключается к RESTful API с помощью Retrofit 2.

Надеюсь, вы узнали, как подключить Android к ASP.NET Core Web API.

Полный исходный код API и проекта приложения можно найти на GitHub .

Не стесняйтесь оставлять свои отзывы в разделе комментариев, и если вам понравился этот урок, пожалуйста, используйте кнопку социальных сетей слева, чтобы поделиться им со своим сообществом.

Подпишитесь на свою электронную почту, чтобы первыми получать обновления и новые статьи, которые я добавляю в этот блог.