在android jetpack中创建基于组件的架构

Motivation: You might have run through apps like Netflix/Amazon Prime, where an individual screen contains a lot of views that are re-used throughout the application. For example, consider the Amazon Prime Video application.

动机:您可能已经运行过Netflix / Amazon Prime等应用程序,其中单个屏幕包含很多视图,这些视图在整个应用程序中都可以重复使用。 例如,考虑使用Amazon Prime Video应用程序。

Image for post
Home screen of Amazon Prime Video
Amazon Prime Video的主屏幕

Consider the home screen, which contains the list of Watch next movies, Thriller Movies, Recommended Movies, and Latest Movies, stacked one below the other. All the above have the same UI with different data sets.

考虑主屏幕, 其中包含“ 观看下一部电影”,“惊悚电影” ,“ 推荐电影 ”和“ 最新电影”的列表,这些列表一个接一个地堆叠。 以上所有内容都具有相同的用户界面,但数据集不同。

Image for post

Consider the movie screen, which contains the overview of the movie and below that, you can find the Customer also watched section. It’s the same UI that was used on the home screen. Thus, parts of the view are re-used throughout the application.

考虑电影屏幕, 其中包含电影的概述,在其下方,您可以找到“ 客户还看过”部分。 与主屏幕上使用的UI相同 因此,视图的一部分将在整个应用程序中重复使用。

设计屏幕 (Designing the Screens)

How would you design such screens?

您将如何设计此类屏幕?

天真的方式 (Naive way)

One way of going about creating the above screens is by hardcoding the views for each of the screens. But we run into problems.

创建上述屏幕的一种方法是对每个屏幕的视图进行硬编码。 但是我们遇到了问题。

Firstly, our domain or presentation layer cannot have the logic to alter the views. We end up changing a lot of files if we just want to add something like the cast members in the UI of the screen.

首先,我们的域或表示层不能具有更改视图的逻辑。 如果我们只想在屏幕的UI中添加演员表之类的内容,则最终会更改大量文件。

Secondly, the problem of duplication.

第二,重复问题。

With the above approach, we end up duplicating the code to render the list of movies on the home screen and the movie screen. The moment you hear duplication, we move them out to a commonplace.

通过上述方法,我们最终复制了代码以在主屏幕和电影屏幕上呈现电影列表。 当您听到重复时 ,我们会将它们移到平凡的地方。

With the proposed architecture (below)we can overcome the above problems by having the duplicate code sit in a commonplace called UIComponents.

使用下面提出的体系结构,我们可以通过将重复的代码放在称为UIComponents的普通地方来克服上述问题。

什么是UIComponent? (What are UIComponents?)

The pieces of UI that can be re-used and are standalone are called UIComponents. You can plug them anywhere in the app on any of the screens and you are good to go.

可以重复使用独立的UI片段称为UIComponents 。 您可以将它们插入任何屏幕上的应用程序中的任何位置,一切都很好。

什么是基于组件的体系结构? (What is a component-based architecture?)

Here, the screen of an Activity is made up of bunch of reusable UIComponents. The Activity is not responsible for creating the views in its XML file. Rather it only includes the UIComponents.

在这里,活动的屏幕由一堆可重用的UIComponent组成。 活动不负责在其XML文件中创建视图。 而是仅包括UIComponents。

In this article, we focus only on rendering the UIComponents on the screen. Later, we look at handling the interactions of the UIComponents with the outside world here.

在本文中,我们仅专注于在屏幕上呈现UIComponent。 稍后,我们在这里查看如何处理UIComponent与外界的交互。

Image for post
Movie-screen UIComponents
电影屏幕UIComponent

Android Jetpack撰写 (Android Jetpack Compose)

Jetpack Compose is a design toolkit to simplify UI development in Android. It is fully declarative, meaning you describe your UI by calling a series of functions that transform data into a UI hierarchy — definition

Jetpack Compose是一个设计工具包,用于简化Android中的UI开发。 它是完全声明式的,这意味着您可以通过调用一系列将数据转换为UI层次结构的函数来描述UI- 定义

In Jetpack Compose, views are composable functions.

在Jetpack Compose中,视图是可组合的功能。

This is how you define a view in Jetpack Compose:

这是您在Jetpack Compose中定义视图的方式:

@Composable
fun UIText(text: String) {
    Text(text)
}

Jetpack Compose中基于组件的架构 (Component-Based Architecture in Jetpack Compose)

The remainder of the article will be split into three sections with some code.

本文的其余部分将分为三部分,并提供一些代码。

  1. Defining/building UIComponents.

    定义/构建UIComponent。
  2. The creation of UIComponents by our ViewModel, by supplying the data necessary for the UIComponent.

    由我们的ViewModel通过提供UIComponent所需的数据来创建UIComponent。

  3. Incorporating the UIComponents in the Jetpack Compose activity.

    将UIComponent合并到Jetpack Compose活动中。

The interconnection of the different layers of the app looks like this. We will improvise later.

应用程序不同层的互连看起来像这样。 我们稍后会即兴创作。

Image for post
Component-based architecture in Android Jetpack compose
Android Jetpack中基于组件的架构

For this demo’s purpose, we will be creating an application with the following screens:

为此,我们将使用以下屏幕创建一个应用程序:

  1. Home screen, with popular/top-rated movies.

    主屏幕,带有热门/顶级电影。
  2. Movie detail screen, with movie overview, cast/crew, recommended, and similar movies.

    电影详细信息屏幕,包括电影概述,演员/演员,推荐电影和类似电影。

1.构建UIComponent (1. Building UIComponents)

A handy composable function type alias for composable views. (Remember? Views are composable functions.)

用于可组合视图的方便的可组合函数类型别名。 (还记得吗?视图是可组合的函数。)

typealias ComposableView = @Composable() () -> Unit

The UIComponent interface is to be implemented by all the UIComponents. This the basic version of our UIComponent. We will modify this later when we start dealing with the interactions.

UIComponent接口将由所有UIComponent实施。 这是我们UIComponent的基本版本。 我们将在以后开始处理交互时对此进行修改。

// All our components should implement this interface
interface UIComponent {
    
    @Composable
    fun createView(): ComposableView
}

To better suit our purpose and for explaining things better, we will be building a MovieDetailScreen. It has the following UIComponents:

为了更好地满足我们的目的并更好地解释事物,我们将构建一个MovieDetailScreen 。 它具有以下UIComponents:

  • Movie overview (MovieOverviewUIComponent)

    电影概述( MovieOverviewUIComponent )

  • Movie cast (CastListUIComponent)

    电影演员表( CastListUIComponent )

  • Movie crew (CrewListUIComponent)

    电影摄制组( CrewListUIComponent )

  • Similar movies (MovieListUIComponent)

    类似电影( MovieListUIComponent )

  • Recommended movies (MovieListUIComponent)

    推荐的电影( MovieListUIComponent )

Image for post
Upper screen with MovieOverview, CastList, and CrewList
带有MovieOverview,CastList和CrewList的上部屏幕
Image for post
Bottom screen with similar and recommended movies
底部屏幕包含类似和推荐的电影

Now, we will build the MovieListUIComponent which can be used to render similar movies and recommended movies on the screen.

现在,我们将构建MovieListUIComponent ,该组件可用于在屏幕上渲染相似的电影和推荐的电影。

Let’s add two extension functions for vertical and horizontal scrolling.

让我们为垂直和水平滚动添加两个扩展功能。

// Vertical view with vertical scrolling
@Composable
fun VStack(child: @Composable() () -> Unit) {
    VerticalScroller { Column(modifier = LayoutPadding(4.dp)) { child() } }
}


// Horizontal view with horizontal scrolling
@Composable
fun HStack(child: @Composable() () -> Unit) {
    HorizontalScroller { Row(modifier = LayoutPadding(4.dp)) { child() } }
}

Let’s build a MovieListUIComponent.

让我们构建一个MovieListUIComponent

1. Design MovieView (Composable). This will be a composable function. Gentle reminder: Views are functions in Jetpack Compose.

1.设计MovieView (可组合)。 这将是一个可组合的功能。 温馨提示:视图是Jetpack Compose中的功能。

@Composable
fun MovieView(movie: Movie) {
  // This is for loading the image synchronously. skipping it for now
  val state = ImageState()
  
  Clickable(onClick = {
    // Notifies the View that click event has occured on the movie. 
    // We will handle this later
  }) {
    
        Column(modifier = LayoutPadding(5.dp)) {
          Container(width = 160.dp, height = 160.dp) { DrawImage(image = state.image) }


          Text(movie.title)
        }
    
  }
}

2. Next, we create a horizontally-scrolling list of movies — HMovieListView.kt. We will use the extension function we created before (HStack).

2.接下来,我们创建电影的水平滚动列表— HMovieListView.kt 我们将使用之前创建的扩展功能( HStack )。

@Composable
fun HMovieListView(movieList: List<Movie>) {
    HStack {
        movieList.forEach { movie ->
            MovieView(movie = movie)
        }


    }
}

3. Now that View has been created, we can create a MovieListUIComponent. The component receives its data (movieList) from the ViewModel. This data is used by the HMovieListView.

3.现在已经创建了View ,我们可以创建一个MovieListUIComponent 。 该组件从ViewModel接收其数据( movieList )。 该数据由HMovieListView

class MovieListUIComponent(
    private val movieList: List<Movie>
) : UIComponent {


    override fun createView(): ComposableView = {
        HMovieListView(movieList = movieList)
    }
}

Our first part is over, we have created the UIComponents. You can get the complete code on GitHub.

第一部分结束,我们创建了UIComponents。 您可以在GitHub上获取完整的代码。

2.构建表示层 (2. Building the Presentation Layer)

Note: The code below is actually simpler than you see here when you look at the complete ViewModel.

注意:实际上,下面的代码比查看完整的ViewModel时的代码简单。

1. Let’s create a single data class to hold the data for the UI, MovieDetailPageUiModel. It’s immutable (all are vals). We use the reducers to update the state. It contains all the UIComponents that are needed for the view. Have a look below.

1.让我们创建一个数据类来保存UI的数据MovieDetailPageUiModel 。 它是不可变的(所有都是val )。 我们使用化简器更新状态。 它包含视图所需的所有UIComponent。 看看下面。

data class MovieDetailPageUiModel(
    val movieDetailUIComponent: MovieDetailUIComponent?,
    val crewUIComponent: CrewUIComponent?,
    val castUIComponent: CastUIComponent?,
    val recommendedMoviesListUIComponent: MovieListUIComponent?,
    val similarMoviesListUIComponent: MovieListUIComponent?
)

2. In ViewModel, this will be MutableLiveData. This holds the complete state of that particular Jetpack Compose activity.

2.在ViewModel ,这将是MutableLiveData 。 这将保留该特定Jetpack Compose活动的完整状态。

class MovieDetailPageViewModel() : ViewModel() { 
  ...
  private val _pageData: MutableLiveData<MovieDetailPageUiModel> = MutableLiveData()
  ...
  
  fun loadRecommendedMovies() { ... }
  fun loadSimilarMovies() { ... }
  
}

3. ViewModel is a data holder and is responsible for creating the UIComponents. As pointed out earlier, ViewModel fetches the data from multiple data sources (API or persistence) which would be necessary to create the components.

3. ViewModel是一个数据持有者,负责创建UIComponent。 如前所述, ViewModel从创建组件所需的多个数据源(API或持久性)中获取数据。

When we get the data, we create our UIComponents and update (reduce) our mutableLiveData.

当我们获取数据时,我们创建我们的UIComponents并更新(减少)我们的mutableLiveData

E.g.: When we fetch the recommended movies, we create the MovieListUIComponent and update the recommendedMoviesListUIComponent value in the movieDetailPageUiModel.

例如:当我们获取推荐的电影,我们创造的MovieListUIComponent和更新recommendedMoviesListUIComponent在价值movieDetailPageUiModel

This goes on for all the values in the movieDetailPageUiModel. Have a look at the code, which makes it clear:

这适用于movieDetailPageUiModel中的所有值。 看一下代码,这很清楚:

class MovieDetailPageViewModel() : ViewModel() {
  
  private fun loadRecommendedMovies1(id: Long) {
        viewModelScope.launch {
            val recommendedMovies: List<Movie> = repository.getRecommendedMovies(id).movies
            _pageData.value = _pageData.value?.copy(
                recommendedMoviesListUIComponent = MovieListUIComponent(recommendedMovies)
            )
        }
    }
}

That’s it! You can have a look at our complete ViewModel on GitHub.

而已! 您可以在GitHub上查看我们完整的ViewModel

3.构建最后一部分-视图(Jetpack撰写活动) (3. Building the Last Part — View (Jetpack Compose Activity))

Aim: The activity should render all the UIComponents in a vertical fashion like a RecyclerView. Currently, we mimic the RecyclerView by using VStack. But you should consider using AdapterList.

目的:该活动应以垂直方式(如RecyclerView呈现所有UIComponent。 当前,我们通过使用VStack模仿RecyclerView 。 但是您应该考虑使用AdapterList

  1. In our MovieDetailPageView, we have a vertical scroller that renders all the UIComponents vertically. The View observes for the change in data and updates itself.

    MovieDetailPageView ,我们有一个垂直滚动器,可垂直显示所有UIComponent。 视图会观察数据的变化并自行更新。

Have a look at how we iterate through the list of UIComponents and render each of them in a vertical fashion. (Similar to multiple view types in a RecyclerView, where each UIComponent is a different view type.)

看一下我们如何遍历UIComponents列表并以垂直方式呈现它们。 (类似于RecyclerView多种视图类型,其中每个UIComponent是不同的视图类型。)

@Composable
fun MovieDetailPageView(data: LiveData<MovieDetailPageUiModel>) {
    val pageUiModel = observe(data = data) // observe the live data. Whenever the value changes it trigers recomposition


    VStack {


        pageUiModel?.components()?.forEach {
            it.createView().render()
        }
    }
}

2. Continuing with the above point. A small change would be required in the HomePageUIModel. We add a function (components()) that returns the list of UIComponents.

2.继续以上几点。 在HomePageUIModel需要进行一些小的更改。 我们添加了一个函数( components() ),该函数返回UIComponents的列表。

data class MovieDetailPageUiModel() {
  /**
 * All the components that needs to be rendered are filtered here.
 * The order of the UIComponents are decided here
 */
  fun components(): ArrayList<UIComponent> {
    val cmps = arrayListOf<UIComponent>()


    cmps.addNonNull(movieDetailUIComponent)
    cmps.addNonNull(crewUIComponent)
    cmps.addNonNull(castUIComponent)
    cmps.addNonNull(recommendedMoviesListUIComponent)
    cmps.addNonNull(similarMoviesListUIComponent)


    return cmps as ArrayList<UIComponent>
  }
}




//Also consider this extension function
fun ArrayList<UIComponent>.addNonNull(uIComponent: UIComponent?): List<UIComponent> {
    if (uIComponent == null) return this
    add(uIComponent)
    return this
}

3. Observe the HomePageUIModel (pageData in our ViewModel) and update the view whenever it changes.

3.观察HomePageUIModel (在ViewModel pageData ),并在视图更改时对其进行更新。

class MovieDetailActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        val movieId = intent.extras?.getLong(MOVIE_ID_REC) ?: throw IllegalArgumentException("Please pass `$MOVIE_ID_REC` parameter")
        vm.load(movieId)
        setContent {
           MovieDetailPageView(vm.pageData)
        }
    }
}

You can have a look at the code on GitHub.

您可以查看GitHub上的代码。

Server-Driven UI: Additionally, we can drive the UI through the API response. Instead of creating the UIComponents in the ViewModel, we move it to a common-place as shown below . UIComponents are created based on the server response.

服务器驱动的用户界面:此外,我们可以通过API响应来驱动用户界面。 与其在ViewModel中创建UIComponents,不如将它移到一个普通的地方,如下所示。 UIComponent是根据服务器响应创建的。

fun ApiResponse.toUIComponents(): List<UIComponents> {
   val uiComponents = mutableListOf<UIComponent>()
   
   // results is an array containing different components to be rendered
   this.results.forEach { component ->
       if(component.TYPE == 'A') {
            uiComponents.add(createComponentTypeA(component.DATA))
       }
       
       if(component.TYPE == 'B') {
            uiComponents.add(createComponentTypeB(component.DATA))
       }
       ..
       ..
   }
   
   return uiComponents
}

结论 (Conclusion)

Following this, we can move out the UIComponents into a separate library which can be reused across different applications.

接下来,我们可以将UIComponents移到一个单独的库中,该库可以在不同的应用程序之间重用。

Currently, we have only laid the UIComponents on the screen. In the next article, the interaction on the UIComponents click events is handled👇🏼.

当前,我们仅将UIComponents放置在屏幕上。 在下一篇文章中,将处理UIComponents click事件上的交互。

翻译自: https://medium.com/better-programming/create-a-component-based-architecture-in-android-jetpack-compose-96980c191351

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值