Giter VIP home page Giter VIP logo

cycler's Introduction

Square Cycler – a RecyclerView API

The Square Cycler API allows you to easily configure an Android RecyclerView declaratively in a succinct way.

Design principles

  • It should be declarative. You tell us what you want, not what to do.
  • It should have all the code regarding one type of row together. The less switch statements the better (some existing libraries and Android recycler itself group all creation together, and all binder together elsewhere; that's close to the metal but far from developer needs).
  • It should be able to cover common needs, specially making adapter access unnecessary. Access to the RecyclerView for ad-hoc configuration is allowed.
  • It should be strongly typed.
  • It should include common features: edge decoration, sticky headers, etc.
  • It should make it easy to inflate rows or to create them programmatically.
  • It should make it easy to create common custom items.

How to use it

  • Configure the recycler view when you create your view.
  • Provide data each time it changes.

Configuring block

The configuring block is the essence of the recycler view. It contains all the row definitions and how to bind data.

You can ask the API to create the RecyclerView object for you – using the create method – or configure an existing instance – through the adopt method. The latter is useful if you already have a layout which the recycler view is part of.

Examples:

val recycler = Recycler.create<ItemType>(context, id = R.id.myrecycler) {
  ...
}
val recycler = Recycler.adopt(findViewById(R.id.my_recycler)) {
  ...
}

In both cases you will receive a Recycler object which represents the RecyclerView and allows you to set data afterwards.

The configuring block will have some general configurations, for instance an item comparator, and a row definition for every type of row you need.

Generics

The generics used along this documentation are as follow:

  • I: ItemType. General type for all the data items of the rows.
  • S: ItemSubType. Data item type for the particular row being defined.
  • V: ViewType. View type for the particular row being defined.

Row definitions

Using a layout:

row<I, S, V> {
  forItemsWhere { subitem -> ...boolean... }
  create(R.layout.my_layout) {
    // you can get references to sub-elements inside view
    val subView = view.findViewById(...)
    bind { subItem ->
      // assign values from subItem to view or sub-elements
    }
  }
  ...more row options...
}

The subtype S will automatically make the row definitions only be used for that type of item I.

forItemsWhere clause is optional. In case you need to filter by an arbitrary predicate on S (notice you don't need to cast).

create will inflate the layout and assign it to a var view: V. You can get references to sub-components using findViewById.

bind receives the subItem (again, already cast to S). You can use view and your own captured references from the create block to assign values. Notice that you don't need to cast view as V. It's already of that type.

General approach:

row<I, S, V> {
  forItemsWhere { subitem -> ...boolean... }
  create { context ->
    view = MyView(context)
    // you can get references to sub-elements inside view
    val subView = view.findViewById(...)
    bind { subItem ->
      // assign values from subItem to view or sub-elements
    }
  }
  ...more row options...
}

This is the general case. Instead of inflating a layout, create provides a context for you to create a view of type V and assign it to view. As usual, you can use that view reference or any other reference you've obtained inside the bind block.

Extra item definitions

Recycler views allow for the inclusion of one extra (but optional) item. This is useful when you want to show your state. For example: "no results" or "loading more...". The extraItem is independent from the main data list and doesn't need to be of type I.

Definitions for extraItems are analogous to normal rows and follow the same convention. However, the definitions are only applied to the extra item you provide along with the data (if any).

extraItem<I, S, V> {
  forItemsWhere { subitem -> ...boolean... }
  create { context ->
    ...
    bind { subItem -> ... }
  }
  ...more row options...
}

Notice that you can define several different extraItem blocks, with the same or different sub-types S and optional forItemWhere.

bind is also provided in case your extra item has data. Imagine you are filtering by fruit. If you've selected "apples" you want to show "No more apples" instead of "No more fruits". That can be achieved with an extra item of type NoMore(val fruitName: String).

More row options

Recycler API offers an extension mechanism. Extensions are useful for cross-cutting concerns like edges or headers which will be discussed separately.

These extensions will be configured in the same way, through a definition block.

Extensions might offer special configuration for certain types of rows. For example, edges can define a default edge configuration, but use different values for the rows of type Banana. In that case the row<Banana> definition will include its special configuration.

See extensions section for more details.

General configuration

The RecyclerView uses certain general definitions that can be configured here as well.

stableIds { item -> ...long... }

If you provide a function that returns an id of type Long for every item in the data, the recycler view will be able to identify unchanged items when data is updated, and animate them accordingly.

itemComparator = ...

When data is updated the RecyclerView compares both datasets to find which item moved where, and check if they changed any data at all.

Android's RecyclerView's can do that calculation but it needs to compare the items. The developer must provide the comparison. You can provide an ItemComparator implementation which is simpler than the required DiffUtil.Callback one.

An ItemComparator provides two methods:

  • areSameIdentity returns true if they represent the same thing (even if data changed).
  • areSameContent tells if any data changed, requiring re-binding.

If your items are Comparable or you have a Comparator you can create an automatic ItemComparator. Just use:

  • fun itemComparatorFor(Comparator<T>): ItemComparator<T>
  • fun naturalItemComparator(): ItemComparator<T> if T is Comparable<T>

It will implement both: identity and content-comparison based on Comparator or Comparable. That means that items will either be different or identical, therefore never updated. But for immutable (or practically immutable) items it works pretty well.

Data providing

Once you configured your recycler view you just need to give it data.

The Recycler object returned by the configuring block represents your recycler view. It has three properties:

  • view: the RecyclerView. You can add it to your layout if it was created by the API.
  • data: the list of items to show.
  • extraItem: the extra item to add to the end (or null).

Notice that data is of type DataSource<I>.

DataSource is a simplified List interface:

interface DataSource<out T> {
  operator fun get(i: Int): T
  val size: Int
}

You can convert an Array or a List to a DataSource using the extension method toDataSource(): arrayOf(1, 2, 3).toDataSource().

The advantage over requiring a Kotlin List is that you can implement your arbitrary DataSource without having to implement the whole List interface, which is bigger.

Extensions

Extensions are a mechanism to add simple-to-configure features to Recyclers without adding dependencies to this library.

Row type extensions

You can create extensions for common custom views in your project:

myCustomItem<I, S> {
  forItemsWhere { ... }
  bind { item, view ->
    view.title  = ...
    view.message = ...
    ...
  }
}

The extension method just needs to use a different row definition method that lets you define how to create the view by separate.

For instance:

/**
 * Extension method for a custom item, allowing full control.
 * ```
 * myCustomItem<I, S> { // this: BinderRowSpec<...>
 *    // you can configure extra stuff:
 *   forItemsWhere { ... }
 *   // and then define your bind lambda:
 *   bind { item, view ->
 *     view.title  = ...
 *     view.message = ...
 *     ...
 *   }
 * }
 * ```
 */
@RecyclerApiMarker
inline fun <I : Any, reified S : I> Recycler.Config<I>.myCustomItem(
  crossinline specBlock: BinderRowSpec<I, S, CustomView>.() -> Unit
) {
  row(
      creatorBlock = { creatorContext ->
        CustomView(creatorContext.context)
        .apply { ... }
      },
      specBlock = specBlock
  )
}

/**
 * Extension method for passing just a bind lambda.
 * ```
 * myCustomItem<I, S> { item, view ->
 *   view.title  = ...
 *   view.message = ...
 *   ...
 * }
 * ```
 */
 @RecyclerApiMarker
 inline fun <I : Any, reified S : I> Recycler.Config<I>.myCustomItem(
   noinline bindBlock: (S, CustomView) -> Unit
 ) {
   row(
       creatorBlock = { creatorContext ->
         CustomView(creatorContext.context)
        .apply { ... }
       },
       bindBlock = bindBlock
   )
 }

Notice:

  • You don't need to declare extension methods for each row. It's just a shorthand for those things your project uses repeatedly.
  • You can also use analogous methods that provide the index of the item in binding.

Decoration extensions

TODO: code and documentation need to be added.

License

Copyright 2019 Square Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

cycler's People

Contributors

afollestad avatar armaxis avatar helios175 avatar ijwhelan avatar luis-cortes avatar trunkator avatar vrallev avatar zach-klippenstein 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  avatar  avatar  avatar  avatar

cycler's Issues

Sample App throws IllegalArgumentException from SimplePage's config (on Recycler.adopt)

I tried launching the Sample App as-is from a fresh clone, unfortunately, I keep getting IllegalArgumentException at inline fun <I : Any> adopt( at the very bottom of Recycler.kt. I am not sure what is happening. From my debugging it seems that the layout is lost (I have no idea how), between passing the recycler view to adopt and actually using the recycler view in the function. I've cloned the repository twice to make sure but it could be an error on my end but I am not sure what causes it. I've added the val v =view for debbuging purposes

This is before getting into adopt from SimplePage config
l1

This is right after going into adopt
l2

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.squareup.cycler.sampleapp, PID: 11779
    java.lang.IllegalArgumentException: RecyclerView needs a layoutManager assigned. Assign one to the view, or pass a layoutProvider argument.
        at com.squareup.cycler.sampleapp.SimplePage.config(SimplePage.kt:117)
        at com.squareup.cycler.sampleapp.RecyclerActivity.onItemSelected(RecyclerActivity.kt:37)
        at android.widget.AdapterView.fireOnSelected(AdapterView.java:957)
        at android.widget.AdapterView.dispatchOnItemSelected(AdapterView.java:946)
        at android.widget.AdapterView.access$300(AdapterView.java:55)
        at android.widget.AdapterView$SelectionNotifier.run(AdapterView.java:910)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Cycler doesn't permit use of StateRestorationPolicy

The library hides the implementation details of the RecyclerView.Adapter usage, making it impossible to assign the RecyclerView.Adapter.StateRestorationPolicy to the adapter. This means we can't resolve state restoration issues the policy is intended to address.

Part of the problem is the adapter is assigned "just-in-time" in com.squareup.cycler.Recycler.update, and there doesn't appear to be any way to hook into this mechanism to modify the adapter prior to assignment.

Is there anything that can be done about this? Can this hack be removed, now that StateRestorationPolicy is available?

Compatibility with shared transitions

Hello,

First off thanks for a really nice library! I've been working with this library over that last few days and it's been a breeze. I currently hit a problem and would like to hear your point of view of it.

Imagine the following setup: Fragment A (with RecyclerView and Recycler obj) -> Fragment B (DetailFragment). Then using a shared element transition from Fragment A to Fragment B, for details how this is done see this article

Currently I create the Recycler object in onCreateView of Fragment A, because adopt need a RecyclerView instance. ( I hope this is correct? )

Once navigation from A -> B -> A happens the view of Fragment A will be destroyed and get created again when the user presses back. Along with the view the Recycler object will also be recreated due to it needing the newly created RecyclerView. This also means the adapter will be recreated but w/o it's old data. In order to make the transition to work one have to populate the Recycler object with the old data that was used before the view was destroyed. Normally the adapter would be created separately and this data would be retained inside and not recreated with the view.

How do we handle a new view being created with this library? Currently my workaround for this looks something along the ways like this:

    private lateinit var recycler: Recycler<DevicesRow>
    private var data = emptyList<ItemRow>().toDataSource()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = AFragmentBinding.inflate(inflater)
        val recyclerView = binding.recyclerView
        recycler = Recycler.adopt(recyclerView) {
             ...
        }
        recycler.data = data
        postponeEnterTransition()
        rv.doOnPreDraw {
            startPostponedEnterTransition()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        data = recycler.data
    }

    override fun onResume() {
        // I'm starting to observe my presenter here
    }

    override fun onPause() {
        // Stopping to observe my presenter here
    }

Worth noting is that onResume() won't be called unless startPostponedEnterTransition() is called first in onCreateView() or onViewCreated(), if we want the animation to work the adapter also needs to have the data when it is called. The solution works for now, but it doesn't really feel like it is the optimal way to recreate the entire Recycler object along with the view?

ArrayIndexOutOfBoundsException thrown when subtype has an undefined RowSpec

This can be reproduced easily by creating another subclass of BaseItem and adding it to the sample list in SimplePage without changing the config.

BaseItem.kt

  data class NewType(
    val id: Int,
    override val amount: Float
  ): BaseItem()

SimplePage.kt

 private fun sampleList() =
    listOf(
        NewType(6, 7f),
        Discount(5, -5f, isStarred = true)
    )

Stack Trace

java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
    at java.util.ArrayList.get(ArrayList.java:439)
    at com.squareup.cycler.Recycler$Adapter.getItemViewType(Recycler.kt:443)

Can we get another exception thrown here make the issue more obvious?

Fantastic work on this library! I refuse to work with RecyclerView any other way 🙂

Add a default layout manager when using adopt()?

When I started experimenting with this library I initially went with Recycler.create() which also created a layout manager for me. Then I decided that I don't want to manually call .addView() to put RV into my parent layout, so I placed RV in xml and called .adopt(findViewById()). Suddenly it crashed, because .adopt() doesn't create a layout manager, and RV throws an exception.

This feels a bit asymmetric and not intuitive. Would it be possible to add a layoutProvider argument to .adopt? I guess it could default to RV existing layoutManager...

My point is that it shouldn't matter if I created RV programmatically or inflated it from xml it should behave the same. Most other View work this way, e.g. FrameLayout doesn't apply some additional styling or whatever based on if I'm inflating or creating it from code.

Swipe customization

Allow for customization of the swipe item-type-wise.

For that:

  • MutationExtensionSpec should have a RowSpec.
  • Particularities for each row should be stored in that object.
  • The swipe system should first consider the configuration inside the particular RowSpec and default to the current one.

Why not use ListAdapter?

Great library. I was just wondering why RecylcerView.Adapter and DiffUtil were used instead of ListAdapter and DiffUtil.ItemCallback? Was it to leverage coroutines for diffing operations?

Recycler should defer update calls that are received while binding views

I am getting an IllegalStateException during the adapter notifications in the launched coroutine in the update block because I had injected the Dispatchers.main.immediate CoroutineDispatcher instead of the Dispatchers.main CoroutineDispatcher so this coroutine for layout wasn't posted back to the main thread but run synchronously leading to the exception during layout.

Now, using the immediate dispatcher may be 'user error' but we could protect against it with a yield() in update()'s launch.

Add travis CI

Configure a simple travis build that runs:

  1. compilation
  2. unit tests
  3. instrumentation test (for a soon to be sample app #16 ).

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.