Giter VIP home page Giter VIP logo

bento's Introduction

A delicious framework for building modularized Android user interfaces, by Yelp.

Maven Central Build Status Twitter License

Bento is a framework inspired by Epoxy for building complex, modularized Android user interfaces. By leveraging the mechanics and best practices behind RecyclerViews on Android, Bento makes it easy to compose various resusable visual components into a single screen. At Yelp, we've been using Bento for our most critical and complex screens in both our consumer and business owner Android applications.

How does it work?

Most Android apps that have a list-based user interface use a RecyclerView to display their views. At a basic level, the RecyclerView works by referencing an ordered list of data and creating a view on screen for each item of data in that list. That works really well if your list consists of homogenous data types, but can quickly become unruly when you need to manage an unbounded number of data and view types to be rendered in a list. It also becomes an issue if you need to use the same view type in a different type of user interface other than a RecyclerView, such as a ViewPager or a ListView.

Bento aims to fix these issues by providing a framework to manage the the complexity of handling different view types and the dynamic position of each view in the list. Bento can also be used to manage views in other parent view types such as ViewPagers and ListViews all while keeping the benefits of RecyclerView best practices like view holders and view recycling.

Bento groups different view types and the logic associated with displaying and interacting with those view types into "Components". A Component can be anything from a simple text view to a horizontal carousel comprised of other components.

At its core, a Component is a self-contained element that generates a data item. An associated ComponentViewHolder class will inflate a view and bind the data item provided to the inflated view. The view holder will also typically bind the Component to the view to handle any user interactions.

We can also create groupings of different components using a ComponentGroup, which is also a Component itself, to keep logical groupings of components together in the list.

The order of a Component in its parent view relative to other components is determined by the ComponentController. This interface is the magic soy sauce that allows us to add, remove and insert components dynamically into the ordering as if we were manipulating values in a simple list data structure. It provides an abstraction we can use to apply this functionality to different view types such as RecyclerView, ListView and ViewPager. For example the RecyclerViewComponentController handles the complex coreography of communicating with the RecyclerView class on determining spans and positions and makes it very simple to manage diverse sets of components in a list.

The Bento framework makes it easy to break down complex interfaces into a set of easy to understand, modular, dynamic and testable components.

Features

  • Modular - Independent sections of a screen should be, well, independent. Bento components are self-contained.
  • Testable - The separation of concerns between a component's parts makes it easy to write unit tests for presenter logic and espresso tests for view binding logic, enabling complete test coverage.
  • Reusable - Bento components can be shared across screens in your app, making code sharing simple.
  • Progressive - No need to rewrite your app from scratch. Bento components can be integrated progressively into your existing application. It even works with ListViews.
  • Scalable - Perfect for large screens with a long list of heterogeneous views. View recycling also helps maximize performance.
  • Low build overhead - No annotation processing means fast build times.

Examples

We're compiling a list of examples of how to use Bento. Here's what we have currently and some of the examples we plan to add in the future:

Installation

Bento can be setup with Gradle:

// Top level build.gradle
allprojects {
	repositories {
		mavenCentral()
	}
}

// Module level build.gradle
dependencies {
    implementation "com.yelp.android:bento:<version-number>"
    androidTestImplementation "com.yelp.android:bento-testing:<version-number>"
}

Contributing

We highly encourage contributions from the community, even if you've never contributed to an open source project before! If you see an issue but don't have time to fix it, open an issue in this repository.

Steps for Contributing

0. Requirements

There are a few requirements to compile Bento.

  • Python is needed for the pre-commit hooks to run
  • Java and Kotlin are needed to actually compile the project
  • Android Studio (we recommend >= 3.5.0)

1. Get the code

Fork the this repository, and then clone the code to your local machine.

2. Prepare the git hooks

Once you have the repo on your machine, run the following command from the root of the project.

$ make install-hooks

These git hooks will make sure you're not committing private keys to the repository. In the future we might add more functionality.

3. Create a new branch

We usually like branches to be named:

  • username/issue-number/what-it-do
  • targo/24/fix-item-range

4. Build, change, test, commit

Repeat as necessary. The project should build from within Android Studio and if it doesn't, see the help section below. You should also be able to compile and run the bento-sample-app to test your changes.

You can also run ./gradlew publishToMavenLocal to publish the package to your local maven repo on your machine. From there you can add mavenLocal() before other maven repositories. That way your project can load in the version of Bento you're working on.

We follow the conventional commits specification, and you should write what your changes are about in a clear commit message.

5. Push your changes and open a pull request

First, squash your commits if you have multiple, and make sure your commit message follows the conventional commits convention. Push your branch with the new commits to your cloned repo, then open a pull request through GitHub. Once Travis CI gives it a green light along with a member of our team, it will be merged.

If your reviewer asks for some changes, address the issue and then make sure to amend your previous commit - there should be only one commit per pull request. Push your changes, repeat until you get a green light.

Making a release

We switched to using standard-version to handle our releases. standard-version is a utility for versioning using semver and CHANGELOG generation powered by Conventional Commits.

If you are one of the members and must release a new version, then check its documentation. You will need to install npm if you don't have it. (We recommand using homebrew to do so.)

With npm install, install standard-version globally: npm i -g standard-version

From master, run standard-version.

standard-version will:

  • detect the previous version tag (based on git tags)
  • infer the next version number, based on previous standard commit messages. A breaking change will trigger a major bump, a new feature will trigger a minor bump, and a fix will trigger a patch bump.
  • update the CHANGELOG.md file with the commit messages, as well as the GlobalDependency.kt file with the new version number.
  • create a new commit with these change, and tag it with the version number.

The rest has to be done manually: check that the commit looks good (proper changelog, version number, tag), then push both commit and tags to master.

Publishing to maven is also done manually for now. This process is internal to Yelp, and we have an internal wiki page about deployment to Maven. Check with other Yelp members about how to proceed when in doubt.

Help

  • Open a GitHub issue and tag it with the Question tag
  • Ask a question on StackOverflow and tag it with bento and android

License

Apache 2.0 - Please read the LICENSE file.

bento's People

Contributors

aanandshekharroy avatar abdallaadelessa avatar argo49 avatar carolkng avatar coltin avatar cortinico avatar dwaxemberg avatar lasya91 avatar martinpdd8 avatar matthewthomaspage avatar meliopoulos avatar pauleat24 avatar pumpkinjnn avatar redwarp avatar ritu-agarwal avatar victorslkim avatar yingchen0605 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bento's Issues

Add TabViewPagerComponent

This component is a combination of Tab Layout and View Pager and hence leverages the features of both

CarouselComponent: if not recycled, does not recycles its views

Issue

The CarouselComponent shares the same view pool as the main RecyclerView, which is great.

But when the CarouselComponent is scrolled out of the page, it will not release it's own Components, until it is itself recycled.

Having a few item not recycled is probably not a big deal, but it has another side effect: its components's onViewDetachedFromWindow callbacks are not called, because they are still technically inside their parent, the carousel's recyclerview.

Screenshot_1588259180

In this screenshot, I have the case where my ComponentController like that:

  • ComponentController:
    • ListComponent
      • CardComponent
      • CardComponent
    • CarouselComponent
      • CardComponent
      • CardComponent
    • ListComponent
      • CardComponent
      • CardComponent

As this contains only 1 CarouselComponent, this carousel component will never be recycled, as no other item needs to reuse its ViewHolder.

Proposed solution

I propose, in CarouselComponentViewHolder#onViewDetachedFromWindow, to clear the Controller.

As there is no guaranties that the bind method will be called again, we then need to add a snippet of code in the onViewAttachedToWindow method to restore the controller's element if we detect it was cleared before. And if that is the case, then we need, once more, to scroll to the correct position.

Propagate Carousel's visibility change to its nested children

When a Carousel is scrolled away from view in a recycler view, it's nested components are not notified that they are becoming invisible.

When the carousel get's back into view, its nested component then don't get notified that they are becoming visible again.

This behavior prevents per card actions to be generated

Making recyclerview not scrollable is hard

Right now is not possible to have access to LayoutManager and make changes to its scrollable methods. We could expose scrolling options on the constructor or allow RecyclerViewComponentController to receive a LayoutManager making it more flexible to other use case.

I didn't went through all the controllers but I think either exposing LayoutManager properties or itself entirely are good options to increase flexibility for the recyclerview and decouple RecyclerViewComponentController from LayoutManager

Cleanup GlobalDependencies.kt

Some dependencies, like powermock, are declared into GlobalDependencies.kt but never used.
That should be cleaned

Improve ListComponent item visibility

When a ListComponent has dividers, Bento will send an onItemVisible() and onItemNotVisible() for the dividers as well as the list items.

This causes confusion to consumers of the ListComponent that expect to be able to use the provided index as the index of the items.

We should have an alternative set of methods for ListComponent that only pertain to the items and not the dividers.

Add check for isGap to default implementation of ComponentGroup.mSpanSizeLookup

When testing a page with a gap in the header, I noticed that the page crashed. I believe this is because it doesn't know how to deal with the span of a gap. I'm not quite sure how this works right now, but we should make the method look more like this:

...
public int getSpanSize(int position) {	           
+   if (hasGap(position)) {	
+       return getNumberLanes();	
+   }	
    RangedValue<Component> rangedValue = mComponentAccordionList.rangedValueAt(position);
...

Crash when reordering ListComponent with dividers.

There is a crash if the user attempts to reorder a ListComponent with dividers. This is because the ListComponent implements onItemsMoved and automatically moves the data in mData. The size of mData will always be (about) half of the total size of the component because of the dividers.

I believe the solution to this will be checking if dividers are enabled, and if so, dividing them by 2 when reordering the data.

Adopt conventional commits in our workflow

I would like this repo to adopt conventional commits as a way to enforce that our changelog is consistent and can be used to automatically generate changelogs when we are release new versions.

https://www.conventionalcommits.org/en/v1.0.0/

A quick overview:

The commit contains the following structural elements, to communicate intent to the consumers of your library:

fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in semantic versioning).
feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in semantic versioning).
BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in semantic versioning). A BREAKING CHANGE can be part of commits of any type.
types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.
footers other than BREAKING CHANGE: may be provided and follow a convention similar to git trailer format.

The benefits of this kind of format is that thanks to its structure, tooling exist around it to automatically generate change logs in a readable and structured form based on the commit messages. See https://www.conventionalcommits.org/en/v1.0.0/#tooling-for-conventional-commits

Also, it is possible to add some github actions like https://github.com/outillage/commitsar directly to the project to make sure that every commit follows the standard.

Unable to Update an Item in a ListComponent without Recalling setData

I was looking through the ReorderListActivity to understand how to implement drag and drop and was unable to understand how to update data inside of a ListComponent.

From the example:

val handleComponent = ListComponent(this, ReorderViewHolder::class.java, 2)
handleComponent.setIsReorderable(true)
handleComponent.toggleDivider(false)
handleComponent.setData((0..10).map { 'A'.plus(it).toString() })
handleComponent.setOnItemMovedCallback(object : OnItemMovedCallback<String> {
    override fun onItemMoved(oldIndex: Int, newIndex: Int) {
        Log.i("Reordered", "Item at $oldIndex moved to $newIndex")
    }
})
val componentGroup = ComponentGroup().apply {
    addComponent(LabeledComponent("Drag handle to reorder"))
    addComponent(handleComponent)
}
componentController.addComponent(componentGroup)

This works super great and allows for effectively dragging the items around. However, in my use case I would like to show the index of the current cell to help provide context around which position you are now placing the cells into. My data therefore is a simple data class something like this:

data class CellData {
    var index: Int.
    var text: String
}

What my instinct was to do is update the data in the array, call notifyItemRangeChanged and call it good. But, since the data appears to be copied i don't have a reference to the actual data anymore. As a result, I found myself just calling list.setData(UPDATED_DATA) to get the desired updates however that was causing items to flash. This then led me to removing animations on changes like so.

(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false 

I think ultimately it would be nice to have either a updateItemAt or allow for setData to take a notify boolean. That way you can update the data behind the scenes and then trigger the update on the appropriate cells.

When inserting an item at position 0 after the list is rendered, need to manually scroll up to see the item

It is really a RecyclerView issue: when we insert an item in a RecyclerView at position 0, the RecyclerView does not scroll up or down.
This is of course wanted behaviour in most cases, except for one: when you are at scroll offset 0, it would be better to stay at scroll offset 0 and display the new item.

The use case that I have is the following. You are on the home page of your app. And you query some asynchronous service to query for some notifications banners.
It's like, "hot informations" that are displayed on top of the page.
Your asynchronous service returns info, you create a banner, and insert it at index 0. You will actually need to manually scroll up to see it.

There's a bunch of failing tests

There are a bunch of failing tests in master. Looks like we aren't running all tests as part of CI and have allowed a bunch to break.

Split bento library in two, core and extra library

Proposal

It seems the bento library is getting clogged by classes that correspond to a specific use case.

For readability, we could split the library in two, and publish two artifacts.

One would be the core library, which only contains what is necessary for bento to work: small, concise, would contain the content of the core package.

Anything else, like the paginationlistcomponent, nested component, list component... could be splitted in a different library.

Why?

It feels that a bunch of the component that we include extra are quite opinionated:

  • The list component by default displays separators.
  • The SimpleComponent takes a presenter but no items? (It will return the presenter, but always null for the item)
  • ... and more

These opinion are diluating the simplicity of Bento.

Use a kotlin formatter

We currently do not have a strict code formatting discipline.
I propose we use ktlint as it's a well documented, well maintainted tool.

There is several option to enforce ktlint:

Enable drag and drop reordering

In Android, drag and drop reordering is a standard: https://material.io/design/components/lists.html#behavior

In general, there are two ways to initialize a reorder. Either you can press and hold on a list item, then move it to the desired location and release, or you can touch a "handle" on it that allows you to move it without long pressing.

Ideally, this should be enabled in Bento. Doing so would be relatively simple because the RecyclerView already supports these actions.

Why the **ViewHolder had needed a Presenter?

As I Known that ViewHolder is for display a item ui of entity
so,Why the **ViewHolder had needed a Presenter?
why do not retrocede the business logic to outside(ex:activity) and ViewHolder just handle the event(click、long click)?
Sincerely ask for advice.

Bento List Components don't support divider views horizontally between components.

In a use case where we create a list component with more than 1 lane, enable dividers and specify a divider view, components will only be spaced by the divider vertically. The divider won't show between lanes.

Maybe add an option to set flags for, HORIZONTAL, VERTICAL, BOTH for where we want the divider view to show.
We could also specify separate divider views for horizontal and vertical.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.