Hermann  Frami

Hermann Frami

1655199027

A Faster JavaScript Packager for Serverless Applications

Serverless Jetpack 🚀  

A faster JavaScript packager for Serverless applications.

  • ⚡ Drop-in replacement for serverless package|deploy
  • 💻 Lambda Functions packaging
  • 🍰 Lambda Layers packaging
  • 📦 Per-function packaging
  • 🐉 Monorepo (lerna, yarn workspace) support
  • 🔀 Tunable, multi-cpu parallelization
  • 🔎 Dependency tracing options (faster packaging, slimmer bundles)

Overview

The Serverless framework is a fantastic one-stop-shop for taking your code and packing up all the infrastructure around it to deploy it to the cloud. Unfortunately, for many JavaScript applications, some aspects of packaging are slow, hindering deployment speed and developer happiness.

With the serverless-jetpack plugin, many common, slow Serverless packaging scenarios can be dramatically sped up. All with a very easy, seamless integration into your existing Serverless projects.

Usage

The short, short version

First, install the plugin:

$ yarn add --dev serverless-jetpack
$ npm install --save-dev serverless-jetpack

Add to serverless.yml

plugins:
  - serverless-jetpack

... and you're off to faster packaging awesomeness! 🚀

A little more detail...

The plugin supports all normal built-in Serverless framework packaging configurations in serverless.yml like:

package:
  # Any `include`, `exclude` logic is applied to the whole service, the same
  # as built-in serverless packaging.
  # include: ...
  exclude:
    - "*"
    - "**/node_modules/aws-sdk/**" # included on Lambda.
    - "!package.json"

plugins:
  # Add the plugin here.
  - serverless-jetpack

functions:
  base:
    # ...
  another:
    # ...
    package:
      # These work just like built-in serverless packaging - added to the
      # service-level exclude/include fields.
      include:
        - "src/**"
        - "!**/node_modules/aws-sdk/**" # Faster way to exclude
        - "package.json"

Configuration

Most Serverless framework projects should be able to use Jetpack without any extra configuration besides the plugins entry. However, there are some additional options that may be useful in some projects (e.g., lerna monorepos, yarn workspaces)...

Service-level configurations available via custom.jetpack:

  • base (string): The base directory (relative to servicePath / CWD) at which dependencies may be discovered by Jetpack. This is useful in some bespoke monorepo scenarios where dependencies may be hoisted/flattened to a root node_modules directory that is the parent of the directory serverless is run from. (default: Serverless' servicePath / CWD).
    • WARNING: See our discussion below about the dangers of including files below the current working directory / Serverless servicePath.
    • Layers: Layers are a bit of an oddity with built-in Serverless Framework packaging in that the current working directory is layer.NAME.path (and not servicePath like usual), yet things like include|exclude apply relatively to the layer path, not the servicePath. Jetpack has a similar choice and applies base applies to the root servicePath for everything (layers, functions, and service packaging), which seems to be the best approach given that monorepo consumers may well lay out projects like functions/* and layers/* and need dependency inference to get all the way to the root irrespective of a child layer path.
  • roots (Array<string>): A list of paths (relative to servicePath / CWD) at which there may additionally declared and/or installed node_modules. (default: [Serverless' servicePath / CWD]).
    • Setting a value here replaces the default [servicePath] with the new array, so if you want to additionally keep the servicePath in the roots array, set as: [".", ADDITION_01, ADDITION_02, ...].
    • This typically occurs in a monorepo project, wherein dependencies may be located in e.g. packages/{NAME}/node_modules and/or hoisted to the node_modules at the project base. It is important to specify these additional dependency roots so that Jetpack can (1) find and include the right dependencies and (2) hone down these directories to just production dependencies when packaging. Otherwise, you risk having a slow serverless package execution and/or end up with additional/missing dependencies in your final application zip bundle.
    • You only need to declare roots of things that aren't naturally inferred in a dependency traversal. E.g., if starting at packages/{NAME}/package.json causes a traversal down to node_modules/something then symlinked up to lib/something-else/node_modules/even-more these additional paths don't need to be separately declared because they're just part of the dependency traversal.
    • Layers: Similar to base, both the project/service- and layer-level roots declarations will be relative to the project servicePath directory and not the layers.NAME.path directory.
  • preInclude (Array<string>): A list of glob patterns to be added before Jetpack's dependency pattern inclusion and Serverless' built-in service-level and then function-level package.includes. This option most typically comes up in a monorepo scenario where you want a broad base exclusion like !functions/** or !packages/** at the service level and then inclusions in later functions.
  • concurrency (Number): The number of independent package tasks (per function and service) to run off the main execution thread. If 1, then run tasks serially in main thread. If 2+ run off main thread with concurrency number of workers. (default: 1).
    • This option is most useful for Serverless projects that (1) have many individually packaged functions, and (2) large numbers of files and dependencies. E.g., start considering this option if your per-function packaging time takes more than 10 seconds and you have more than one service and/or function package.
  • collapsed.bail (Boolean): Terminate serverless program with an error if collapsed file conflicts are detected. See discussion below regarding collapsed files.

The following function and layer-level configurations available via functions.{FN_NAME}.jetpack and layers.{LAYER_NAME}.jetpack:

  • roots (Array<string>): This option adds more dependency roots to the service-level roots option.
  • preInclude (Array<string>): This option adds more glob patterns to the service-level preInclude option.
  • collapsed.bail (Boolean): Terminate serverless program with an error if collapsed file conflicts are detected if the function is being packaged individually.

Here are some example configurations:

Additional roots

# serverless.yml
plugins:
  - serverless-jetpack

functions:
  base:
    # ...
  another:
    # This example monorepo project has:
    # - `packages/another/src`: JS source code to include
    # - `packages/another/package.json`: Declares production dependencies
    # - `packages/another/node_modules`: One location prod deps may be.
    # - `node_modules`: Another location prod deps may be if hoisted.
    # ...
    package:
      individually: true
    jetpack:
      roots:
        # If you want to keep prod deps from servicePath/CWD package.json
        # - "."
        # Different root to infer prod deps from package.json
        - "packages/another"
    include:
      # Ex: Typically you'll also add in sources from a monorepo package.
      - "packages/another/src/**"

Different base root

# serverless.yml
plugins:
  - serverless-jetpack

custom:
  jetpack:
    # Search for hoisted dependencies to one parent above normal.
    base: ".."

package:
  # ...
  include:
    # **NOTE**: The include patterns now change to allow the underlying
    # globbing libraries to reach below the working directory to our base,
    # so patterns should be of the format:
    # - "!{BASE/,}{**/,}NORMAL_PATTERN"
    # - "!{BASE/,}{**/,}node_modules/aws-sdk/**"
    # - "!{BASE/,}{**/,}node_modules/{@*/*,*}/README.md"
    #
    # ... here with a BASE of `..` that means:
    # General
    - "!{../,}{**/,}.DS_Store"
    - "!{../,}{**/,}.vscode/**"
    # Dependencies
    - "!{../,}{**/,}node_modules/aws-sdk/**"
    - "!{../,}{**/,}node_modules/{@*/*,*}/CHANGELOG.md"
    - "!{../,}{**/,}node_modules/{@*/*,*}/README.md"

functions:
  base:
    # ...

With custom pre-includes

# 1. `preInclude` comes first after internal `**` pattern.
custom:
  jetpack:
    preInclude:
      - "!**" # Start with absolutely nothing (typical in monorepo scenario)

# 2. Jetpack then dynamically adds in production dependency glob patterns.

# 3. Then, we apply the normal serverless `include`s.
package:
  individually: true
  include:
    - "!**/node_modules/aws-sdk/**"

plugins:
  - serverless-jetpack

functions:
  base:
    # ...
  another:
    jetpack:
      roots:
        - "packages/another"
      preInclude:
        # Tip: Could then have a service-level `include` negate subfiles.
        - "packages/another/dist/**"
    include:
      - "packages/another/src/**"

Layers

# serverless.yml
plugins:
  - serverless-jetpack

layers:
  vendor:
    # A typical pattern is `NAME/nodejs/node_modules` that expands to
    # `/opt/nodejs/node_modules` which is included in `NODE_PATH` and available
    # to running lambdas. Here, we use `jetpack.roots` to properly exclude
    # `devDependencies` that built-in Serverless wouldn't.
    path: layers/vendor
    jetpack:
      roots:
        # Instruct Jetpack to review and exclude devDependencies originating
        # from this `package.json` directory.
        - "layers/vendor/nodejs"

How Jetpack's faster dependency filtering works

Serverless built-in packaging slows to a crawl in applications that have lots of files from devDependencies. Although the excludeDevDependencies option will ultimately remove these from the target zip bundle, it does so only after the files are read from disk, wasting a lot of disk I/O and time.

The serverless-jetpack plugin removes this bottleneck by performing a fast production dependency on-disk discovery via the inspectdep library before any globbing is done. The discovered production dependencies are then converted into patterns and injected into the otherwise normal Serverless framework packaging heuristics to efficiently avoid all unnecessary disk I/O due to devDependencies in node_modules.

Process-wise, the serverless-jetpack plugin detects when built-in packaging applies and then takes over the packaging process. The plugin then sets appropriate internal Serverless artifact fields to cause Serverless to skip the (slower) built-in packaging.

The nitty gritty of why it's faster

Let's start by looking at how Serverless packages (more or less):

  1. If the excludeDevDependencies option is set, use synchronous globby() for on disk I/O calls to find all the package.json files in node_modules, then infer which are devDependencies. Use this information to enhance the include|exclude configured options.
  2. Glob files from disk using globby with a root ** (all files) and the include pattern, following symlinks, and create a list of files (no directories). This is again disk I/O.
  3. Filter the in-memory list of files using nanomatch via service + function exclude, then include patterns in order to decide what is included in the package zip file.

This is potentially slow if node_modules contains a lot of ultimately removed files, yielding a lot of completely wasted disk I/O time.

Jetpack, by contrast does the following:

  1. Efficiently infer production dependencies from disk without globbing, and without reading any devDependencies.
  2. Glob files from disk with a root ** (all files), !node_modules/** (exclude all by default), node_modules/PROD_DEP_01/**, node_modules/PROD_DEP_02/**, ... (add in specific directories of production dependencies), and then the normal include patterns. This small nuance of limiting the node_modules globbing to just production dependencies gives us an impressive speedup.
  3. Apply service + function exclude, then include patterns in order to decide what is included in the package zip file.

This ends up being way faster in most cases, and particularly when you have very large devDependencies. It is worth pointing out the minor implication that:

  • If your include|exclude logic intends to glob in devDependencies, this won't work anymore. But, you're not really planning on deploying non-production dependencies are you? 😉

Complexities

Other Serverless plugins that set package.artifact

The serverless-jetpack plugin hooks into the Serverless packaging lifecycle by being the last function run in the before:package:createDeploymentArtifacts lifecycle event. This means that if a user configures package.artifact directly in their Serverless configuration or another plugin sets package.artifact before Jetpack runs then Jetpack will skip the unit of packaging (service, function, layer, etc.).

Some notable plugins that do set package.artifact and thus don't need and won't use Jetpack (or vanilla Serverless packaging for that matter):

Minor differences vs. Serverless globbing

Our benchmark correctness tests highlight a number of various files not included by Jetpack that are included by serverless in packaging our benchmark scenarios. Some of these are things like node_modules/.yarn-integrity which Jetpack knowingly ignores because you shouldn't need it. All of the others we've discovered to date are instances in which serverless incorrectly includes devDependencies...

Layers

Jetpack supports layer packaging as close to serverless as it can. However, there are a couple of very wonky things with serverless' approach that you probably want to keep in mind:

  • Service level package.include|exclude patterns are applied at the layers.NAME.path level for a given layer. So, e.g., if you have a service-level include pattern of "!*" to remove ROOT/foo.txt, this will apply at a different root path from layers.NAME.path of like ROOT/layers/NAME/foo.txt.
  • As mentioned in our options configuration section above, Jetpack applies the base and roots options to the root project servicePath for dependency searching and not relatively to layer paths.

Be careful with include configurations and node_modules

Let's start with how include|exclude work for both Serverless built-in packaging and Jetpack:

Disk read phase with globby(). Assemble patterns in order from below and then return a list of files matching the total patterns.

  1. Start at ** (everything).
  2. (Jetpack only) Add in service and function-level jetpack.preInclude patterns.
  3. (Jetpack only) Add in dynamic patterns to include production node_modules.
  4. Add in service and function-level package.include patterns.

File filtering phase with nanomatch(). Once we have a list of files read from disk, we apply patterns in order as follows to decide whether to include them (last positive match wins).

  1. (Jetpack only) Add in service and function-level jetpack.preInclude patterns.
  2. (Jetpack only) Add in dynamic patterns to include production node_modules.
  3. Add in service and function-level package.exclude patterns.
  4. (Serverless only) Add in dynamic patterns to exclude development node_modules
  5. Add in service and function-level package.include patterns.

The practical takeaway here is the it is typically faster to prefer include exclusions like !foo/** than to use exclude patterns like foo/** because the former avoids a lot of unneeded disk I/O.

Let's consider a pattern like this:

include:
  - "node_modules/**"
exclude:
  - # ... a whole bunch of stuff ...

This would likely be just as slow as built-in Serverless packaging because all of node_modules gets read from disk.

Thus, the best practice here when crafting service or function include configurations is: don't include anything extra in node_modules. It's fine to do extra exclusions like:

# Good. Remove dependency provided by lambda from zip
exclude:
  - "**/node_modules/aws-sdk/**"

# Better! Never even read the files from disk during globbing in the first place!
include:
  - "!**/node_modules/aws-sdk/**"

Packaging files Outside CWD

How files are zipped

A potentially serious situation that comes up with adding files to a Serverless package zip file is if any included files are outside of Serverless' servicePath / current working directory. For example, if you have files like:

- src/foo/bar.js
- ../node_modules/lodash/index.js

Any file below CWD is collapsed into starting at CWD and not outside. So, for the above example, we package / later expand:

- src/foo/bar.js                # The same.
- node_modules/lodash/index.js  # Removed `../`!!!

This most often happens with node_modules in monorepos where node_modules roots are scattered across different directories and nested. In particular, if you are using the custom.jetpack.base option this is likely going to come into play. Fortunately, in most cases, it's not that big of a deal. For example:

- node_modules/chalk/index.js
- ../node_modules/lodash/index.js

will collapse when zipped to:

- node_modules/chalk/index.js
- node_modules/lodash/index.js

... but Node.js resolution rules should resolve and load the collapsed package the same as if it were in the original location.

Zipping problems

The real problems occur if there is a path conflict where files collapse to the same location. For example, if we have:

- node_modules/lodash/index.js
- ../node_modules/lodash/index.js

this will append files with the same path in the zip file:

- node_modules/lodash/index.js
- node_modules/lodash/index.js

that when expanded leave only one file actually on disk!

How to detect zipping problems

The first level is detecting potentially collapsed files that conflict. Jetpack does this automatically with log warnings like:

Serverless: [serverless-jetpack] WARNING: Found 1 collapsed dependencies in .serverless/my-function.zip! Please fix, with hints at: https://npm.im/serverless-jetpack#packaging-files-outside-cwd
Serverless: [serverless-jetpack] .serverless/FN_NAME.zip collapsed dependencies:
- lodash (Packages: 2, Files: 108 unique, 216 total): [node_modules/lodash@4.17.11, ../node_modules/lodash@4.17.15]`

In the above example, 2 different versions of lodash were installed and their files were collapsed into the same path space. A total of 216 files will end up collapsed into 108 when expanded on disk in your cloud function. Yikes!

A good practice if you are using tracing mode is to set: jetpack.collapsed.bail = true so that Jetpack will throw an error and kill the serverless program if any collapsed conflicts are detected.

How to solve zipping problems

So how do we fix the problem?

A first starting point is to generate a full report of the packaging step. Instead of running serverless deploy|package <OPTIONS>, try out serverless jetpack package --report <OPTIONS>. This will produce a report at the end of packaging that gives a full list of files. You can then use the logged message above as a starting point to examine the actual files collapsed in the zip file. Then, spend a little time figuring out the dependencies of how things ended up where.

With a better understanding of what the files are and why we can turn to avoiding collapses. Some options:

Don't allow node_modules in intermediate directories: Typically, a monorepo has ROOT/package.json and packages/NAME/package.json or something, which doesn't typically lead to collapsed files. A situation that runs into trouble is something like:

with serverless being run from backend as CWD then ROOT/node_modules and ROOT/backend/node_modules will present potential collapsing conflicts. So, if possible, just remove the backend/package.json dependencies and stick them all either in the root or further nested into the functions/packages of the monorepo.

ROOT/package.json
ROOT/backend/package.json
ROOT/backend/functions/NAME/package.json

Mirror exact same dependencies in package.jsons: In our above example, even if lodash isn't declared in either ../package.json or package.json we can manually add it to both at the same pinned version (e.g., "lodash": "4.17.15") to force it to be the same no matter where npm or Yarn place the dependency on disk.

Use Yarn Resolutions: If you are using Yarn and resolutions are an option that works for your project, they are a straightforward way to ensure that only one of a dependency exists on disk, solving collapsing problems.

Use package.include|exclude: You can manually adjust packaging by excluding files that would be collapsed and then allowing the other ones to come into play. In our example above, a negative package.include for !node_modules/lodash/** would solve our problem in a semver-acceptable way by leaving only root-level lodash.

Tracing mode

ℹ️ Experimental: Although we have a wide array of tests, tracing mode is still considered experimental as we roll out the feature. You should be sure to test all the execution code paths in your deployed serverless functions and verify your bundled package contents before using in production.

Jetpack speeds up the underlying dependencies filtering approach of serverless packaging while providing completely equivalent bundles. However, this approach has some fundamental limitations:

  • Over-inclusive: All production dependencies include many individual files that are not needed at runtime.
  • Speed: For large sets of dependencies, copying lots of files is slow at packaging time.

Thus, we pose the question: What if we packaged only the files we needed at runtime?

Welcome to tracing mode!

Tracing mode is an alternative way to include dependencies in a serverless application. It works by using Acorn to parse out all dependencies in entry point files (require, require.resolve, static import) and then resolves them with resolve according to the Node.js resolution algorithm. This produces a list of the files that will actually be used at runtime and Jetpack includes these instead of traversing production dependencies. The engine for all of this work is a small, dedicated library, trace-deps.

Tracing configuration

The most basic configuration is just to enable custom.jetpack.trace (service-wide) or functions.{FN_NAME}.jetpack.trace (per-function) set to true. By default, tracing mode will trace just the entry point file specified in functions.{FN_NAME}.handler.

plugins:
  - serverless-jetpack

custom:
  jetpack:
    trace: true

The trace field can be a Boolean or object containing further configuration information.

Tracing options

The basic trace Boolean field should hopefully work for most cases. Jetpack provides several additional options for more flexibility:

Service-level configurations available via custom.jetpack.trace:

  • trace (Boolean | Object): If trace: true or trace: { /* other options */ } then tracing mode is activated at the service level.
  • trace.ignores (Array<string>): A set of package path prefixes up to a directory level (e.g., react or mod/lib) to skip tracing on. This is particularly useful when you are excluding a package like aws-sdk that is already provided for your lambda.
  • trace.allowMissing (Object.<string, Array<string>>): A way to allow certain packages to have potentially failing dependencies. Specify each object key as either (1) an source file path relative to servicePath / CWD that begins with a ./ or (2) a package name and provide a value as an array of dependencies that might be missing on disk. If the sub-dependency is found, then it is included in the bundle (this part distinguishes this option from ignores). If not, it is skipped without error.
  • trace.include (Array<string>): Additional file path globbing patterns (relative to servicePath) to be included in the package and be further traced for dependencies to include. Applies to functions that are part of a service or function (individually) packaging.
    • Note: These patterns are in addition to the handler inferred file path. If you want to exclude the handler path you could technically do a !file/path.js exclusion, but that would be a strange case in that your handler files would no longer be present.
  • trace.dynamic.bail (Boolean): Terminate serverless program with an error if dynamic import misses are detected. See discussion below regarding handling.
  • trace.dynamic.resolutions (Object.<string, Array<string>>): Handle dynamic import misses by providing a key to match misses on and an array of additional glob patterns to trace and include in the application bundle.
    • Application source files: If a miss is an application source file (e.g., not within node_modules), specify the relative path (from servicePath / CWD) to it like "./src/server/router.js": [/* array of patterns */].
      • Note: To be an application source path, it must be prefixed with a dot (e.g., ./src/server.js, ../lower/src/server.js). Basically, like the Node.js require() rules go for a local path file vs. a package dependency.
    • Dependency packages: If a miss is part of a dependency (e.g., an npm package placed within node_modules), specify the package name first (without including node_modules) and then trailing path to file at issue like "bunyan/lib/bunyan.js": [/* array of patterns */].
    • Ignoring dynamic import misses: If you just want to ignore the missed dynamic imports for a given application source file or package, just specify and empty array [] or falsey value.

A way to allow certain packages to have potentially failing dependencies. Specify each object key as a package name and value as an array of dependencies that might be missing on disk. If the sub-dependency is found, then it is included in the bundle (this part distinguishes this option from ignores). If not, it is skipped without error.

The following function-level configurations available via functions.{FN_NAME}.jetpack.trace and layers.{LAYER_NAME}.jetpack.trace:

  • trace (Boolean | Object): If trace: true or trace: { /* other options */ } then tracing mode is activated at the function level if the function is being packaged individually.
  • trace.ignores (Array<string>): A set of package path prefixes up to a directory level (e.g., react or mod/lib) to skip tracing if the function is being packaged individually. If there are service-level trace.ignores then the function-level ones will be added to the list.
  • trace.allowMissing (Object.<string, Array<string>>): An object of package path prefixes mapping to lists of packages that are allowed to be missing if the function is being packaged individually. If there is a service-level trace.allowMissing object then the function-level ones will be smart merged into the list.
  • trace.include (Array<string>): Additional file path globbing patterns (relative to servicePath) to be included in the package and be further traced for dependencies to include. Applies to functions that are part of a service or function (individually) packaging. If there are service-level trace.includes then the function-level ones will be added to the list.
  • trace.dynamic.bail (Boolean): Terminate serverless program with an error if dynamic import misses are detected if the function is being packaged individually.
  • trace.dynamic.resolutions (Object.<string, Array<string>>): An object of application source file or package name keys mapping to lists of pattern globs that are traced and included in the application bundle if the function is being packaged individually. If there is a service-level trace.dynamic.resolutions object then the function-level ones will be smart merged into the list.

Let's see the advanced options in action:

plugins:
  - serverless-jetpack

custom:
  jetpack:
    preInclude:
      - "!**"
    trace:
      ignores:
        # Unconditionally skip `aws-sdk` and all dependencies
        # (Because it already is installed in target Lambda)
        - "aws-sdk"
      allowMissing:
        # For just the `ws` package allow certain lazy dependencies to be
        # skipped without error if not found on disk.
        "ws":
          - "bufferutil"
          - "utf-8-validate"
      dynamic:
        # Force errors if have unresolved dynamic imports
        bail: true
        # Resolve encountered dynamic import misses, either by tracing
        # additional files, or ignoring after confirmation of safety.
        resolutions:
          # **Application Source**
          #
          # Specify keys as relative path to application source files starting
          # with a dot.
          "./src/server/config.js":
            # Manually trace all configuration files for bespoke configuration
            # application code. (Note these are relative to the file key!)
            - "../../config/default.js"
            - "../../config/production.js"

          # Ignore dynamic import misses with empty array.
          "./src/something-else.js": []

          # **Dependencies**
          #
          # Specify keys as `PKG_NAME/path/to/file.js`.
          "bunyan/lib/bunyan.js":
            # - node_modules/bunyan/lib/bunyan.js [79:17]: require('dtrace-provider' + '')
            # - node_modules/bunyan/lib/bunyan.js [100:13]: require('mv' + '')
            # - node_modules/bunyan/lib/bunyan.js [106:27]: require('source-map-support' + '')
            #
            # These are all just try/catch-ed permissive require's meant to be
            # excluded in browser. We manually add them in here.
            - "dtrace-provider"
            - "mv"
            - "source-map-support"

          # Ignore: we aren't using themes.
          # - node_modules/colors/lib/colors.js [127:29]: require(theme)
          "colors/lib/colors.js": []

package:
  include:
    - "a/manual/file-i-want.js"

functions:
  # Functions in service package.
  # - `jetpack.trace.ignores` does not apply.
  # - `jetpack.trace.include` **will** include and trace additional files.
  service-packaged-app-1:
    handler: app1.handler

  service-packaged-app-2:
    handler: app2.handler
    jetpack:
      # - `jetpack.trace.allowMissing` additions are merged into service level
      trace:
        # Trace and include: `app2.js` + `extra/**.js` patterns
        include:
          - "extra/**.js"

  # Individually with no trace configuration will be traced from service-level config
  individually-packaged-1:
    handler: ind1.handler
    package:
      individually: true
      # Normal package include|exclude work the same, but are not traced.
      include:
        - "some/stuff/**"
    jetpack:
      trace:
        # When individually, `ignores` from fn are added: `["aws-sdk", "react-ssr-prepass"]`
        ignores:
          - "react-ssr-prepass"
        # When individually, `allowMissing` smart merges like:
        # `{ "ws": ["bufferutil", "utf-8-validate", "another"] }`
        allowMissing:
          "ws":
            - "another"

  # Individually with explicit `false` will not be traced
  individually-packaged-1:
    handler: ind1.handler
    package:
      individually: true
    jetpack:
      trace: false

Tracing caveats

Works best for large, unused production dependencies: Tracing mode is best suited for an application wherein many / most of the files specified in package.json:dependencies are not actually used. When there is a large discrepancy between "specific dependencies" and "actually used files" you'll see the biggest speedups. Conversely, when production dependencies are very tight and almost every file is used you won't see a large speedup versus Jetpack's normal dependency mode.

Only works with JavaScript handlers + code: Tracing mode only works with functions.{FN_NAME}.handler and trace.include files that are real JavaScript ending in the suffixes of .js or .mjs. If you have TypeScript, JSX, etc., please transpile it first and point your handler at that file. By default tracing mode will search on PATH/TO/HANDLER_FILE.{js,mjs} to then trace, and will throw an error if no matching files are found for a function that has runtime: node* when tracing mode is enabled.

Only works with imports/requires: trace-deps only works with a supported set of require, require.resolve and import dependency specifiers. That means if your application code or a dependency does something like: const styles = fs.readFileSync(path.join(__dirname, "styles.css")) then the dependency of node_modules/<pkg>/<path>/styles.css will not be included in your serverless bundle. To remedy this you presently must manually detect and find any such missing files and use a standard service or function level package.include as appropriate to explicitly include the specific files in your bundle.

Service/function-level Applications: Tracing mode at the service level and individually configurations work as follows:

  • If service level custom.jetpack.trace is set (true or config object), then the service will be traced. All functions are packaged in tracing mode except for those with both individually enabled (service or function level) and functions.{FN_NAME}.jetpack.trace=false explicitly.
  • If service level custom.jetpack.trace is false or unset, then the service will not be traced. All functions are packaged in normal dependency-filtering mode except for those with both individually enabled (service or function level) and functions.{FN_NAME}.jetpack.trace is set which will be in tracing mode.

Replaces Package Introspection: Enabling tracing mode will replace all package.json production dependency inspection and add a blanket exclusion pattern for node_modules meaning things that are traced are the only thing that will be included by your bundle.

Works with other include|excludess: The normal package include|excludes work like normal and are a means of bring in other files as appropriate to your application. And for many cases, you will want to include other files via the normal serverless configurations, just without tracing and manually specified.

Layers are not traced: Because Layers don't have a distinct entry point, they will not be traced. Instead Jetpack does normal pattern-based production dependency inference.

Static analysis by default: Out of the box, tracing will only detect files included via require("A_STRING"), require.resolve("A_STRING"), import "A_STRING", and import NAME from "A_STRING". It will not work with dynamic import()s or requires that dynamically inject a variable etc. like require(myVariable).

  • Note: Jetpack will log warnings for files found that have dynamic imports that tracing missed. See WARNING log output for the list of files and read our section below on handling dynamic imports.

Handling dynamic import misses

Dynamic imports that use variables or runtime execution like require(A_VARIABLE) or import(`template_${VARIABLE}`) cannot be used by Jetpack to infer what the underlying dependency files are for inclusion in the bundle. That means some level of developer work to handle.

Identify

The first step is to be aware and watch for dynamic import misses. Conveniently, Jetpack logs warnings like the following:

Serverless: [serverless-jetpack] WARNING: Found 6 dependency packages with tracing misses in .serverless/FN_NAME.zip! Please see logs and read: https://npm.im/serverless-jetpack#handling-dynamic-import-misses
Serverless: [serverless-jetpack] .serverless/FN_NAME.zip dependency package tracing misses: [* ... */,"colors","bunyan",/* ... */]

and produces combined --report output like:

### Tracing Dynamic Misses (`6` packages): Dependencies

...
- ../node_modules/aws-xray-sdk-core/node_modules/colors/lib/colors.js [127:29]: require(theme)
- ../node_modules/bunyan/lib/bunyan.js [79:17]: require('dtrace-provider' + '')
- ../node_modules/bunyan/lib/bunyan.js [100:13]: require('mv' + '')
- ../node_modules/bunyan/lib/bunyan.js [106:27]: require('source-map-support' + '')
...

which gives you the line + column number of the dynamic dependency in a given source file and snippet of the code in question.

In addition to just logging this information, you can ensure you have no unaccounted for dynamic import misses by setting jetpack.trace.dynamic.bail = true in your applicable service or function-level configuration.

Diagnose

With the --report output in hand, the recommended course is to identify what the impact is of these missed dynamic imports. For example, in node_modules/bunyan/lib/bunyan.js the interesting require('mv' + '') import is within a permissive try/catch block to allow conditional import of the library if found (and prevent browserify from bundling the library). For our Serverless application we could choose to ignore these dynamic imports or manually add in the imported libraries.

For other dependencies, there may well be "hidden" dependencies that you will need to add to your Serverless bundle for runtime correctness. Things like node-config which dynamically imports various configuration files from environment variable information, etc.

Remedy

Once we have logging information and the --report output, we can start remedying dynamic import misses via the Jetpack feature jetpack.trace.dynamic.resolutions. Resolutions are keys to files with dynamic import misses that allow a developer to specify what imports should be included manually or to simply ignore the dynamic import misses.

Keys: Resolutions take a key value to match each file with missing dynamic imports. There are two types of keys that are used:

  • Application Source File: Something that is within your application and not node_modules. Specify these files with a dot prefix as appropriate relative to the Serverless service path (usually CWD) like ./src/server.js or ../outside/file.js.
  • Package Dependencies: A file from a dependency within node_modules. Specify these files without a dot and just PKG_NAME/path/to/file.js or @SCOPE/PKG_NAME/path/to/file.js.

Values: Values are an array of extra imports to add in from each file as if they were declared in that very file with require("EXTRA_IMPORT") or import "EXTRA_IMPORT". This means the values should either be relative paths within that package (./lib/auth/noop.js) or other package dependencies (lodash or lodash/map.js). * Note: We choose to support "additional imports" and not just file additions like package.include or jetpack.trace.include. The reason is that for package dependency import misses, the packages can be flattened to unpredictable locations in the node_modules trees and doubly so in monorepos. An import will always be resolved to the correct location, and that's why we choose it. At the same time, tools like package.include or jetpack.trace.includeare still available to use!

Some examples:

bunyan: The popular logger library has some optional dependencies that are not meant only for Node.js. To prevent browser bundling tools from including, they use a curious require strategy of require('PKG_NAME' + '') to defeat parsing. For Jetpack, this means we get dynamic misses reports of:

- node_modules/bunyan/lib/bunyan.js [79:17]: require('dtrace-provider' + '')
- node_modules/bunyan/lib/bunyan.js [100:13]: require('mv' + '')
- node_modules/bunyan/lib/bunyan.js [106:27]: require('source-map-support' + '')

Using resolutions we can remedy these by simple adding imports for all three libraries like:

custom:
  jetpack:
    trace:
      dynamic:
        resolutions:
          "bunyan/lib/bunyan.js":
            - "dtrace-provider"
            - "mv"
            - "source-map-support"

express: The popular server framework dynamically imports engines which produces a dynamic misses report of:

- node_modules/express/lib/view.js [81:13]: require(mod)

In a common case, this is a non-issue if you aren't using engines, so we can simply "ignore" the import miss by setting an empty array resolutions value:

custom:
  jetpack:
    trace:
      dynamic:
        resolutions:
          "express/lib/view.js": []

Once we have analyzed all of our misses and added resolutions to either ignore the miss or add other imports, we can then set trace.dynamic.bail = true to make sure that if future dependency upgrades adds new, unhandled dynamic misses we will get a failed build notification so we know that we're always deploying known, good code.

Tracing results

The following is a table of generated packages using vanilla Serverless vs Jetpack with tracing (using yarn benchmark:sizes).

The relevant portions of our measurement chart.

  • Scenario: Same benchmark scenarios
  • Type: jetpack is this plugin in trace mode and baseline is Serverless built-in packaging.
  • Zips: The number of zip files generated per scenario (e.g., service bundle + individually packaged function bundles).
  • Files: The aggregated number of individual files in all zip files for a given scenario. This shows how Jetpack in tracing mode results in many less files.
  • Size: The aggregated total byte size of all zip files for a given scenario. This shows how Jetpack in tracing mode results in smaller bundle packages.
  • vs Base: Percentage difference of the aggregated zip bundle byte sizes for a given scenario of Jetpack vs. Serverless built-in packaging.

Results:

ScenarioTypeZipsFilesSizevs Base
simplejetpack1200529417-42.78 %
simplebaseline1433925260 
complexjetpack215883835544-18.20 %
complexbaseline221204688648 

Command Line Interface

Jetpack also provides some CLI options.

serverless jetpack package

Package a function like serverless package does, just with better options.

$ serverless jetpack package -h
Plugin: Jetpack
jetpack package ............... Packages a Serverless service or function
    --function / -f .................... Function name. Packages a single function (see 'deploy function')
    --report / -r ...................... Generate full bundle report

So, to package all service / functions like serverless package does, use:

$ serverless jetpack package # OR
$ serverless package

... as this is basically the same built-in or custom.

The neat addition that Jetpack provides is:

$ serverless jetpack package -f|--function {NAME}

which allows you to package just one named function exactly the same as serverless deploy -f {NAME} does. (Curiously serverless deploy implements the -f {NAME} option but serverless package does not.)

Benchmarks

The following is a simple, "on my machine" benchmark generated with yarn benchmark. It should not be taken to imply any real world timings, but more to express relative differences in speed using the serverless-jetpack versus the built-in baseline Serverless framework packaging logic.

As a quick guide to the results table:

  • Scenario: Contrived scenarios for the purpose of generating results. E.g.,
    • simple: Very small production and development dependencies.
    • complex: Many different serverless configurations all in one.
  • Pkg: Project installed via yarn or npm? This really only matters in that npm and yarn may flatten dependencies differently, so we want to make sure Jetpack is correct in both cases.
  • Type: jetpack is this plugin and baseline is Serverless built-in packaging.
  • Mode: For jetpack benchmarks, either:
    • deps: Dependency filtering with equivalent output to serverless (just faster).
    • trace: Tracing dependencies from specified source files. Not equivalent to serverless packaging, but functionally correct, way faster, and with smaller packages.
  • Time: Elapsed build time in milliseconds.
  • vs Base: Percentage difference of serverless-jetpack vs. Serverless built-in. Negative values are faster, positive values are slower.

Machine information:

  • os: darwin 18.7.0 x64
  • node: v12.14.1

Results:

ScenarioPkgTypeModeTimevs Base
simpleyarnjetpacktrace4878-74.25 %
simpleyarnjetpackdeps3861-79.62 %
simpleyarnbaseline 18941 
simplenpmjetpacktrace7290-68.34 %
simplenpmjetpackdeps4017-82.55 %
simplenpmbaseline 23023 
complexyarnjetpacktrace10475-70.93 %
complexyarnjetpackdeps8821-75.52 %
complexyarnbaseline 36032 
complexnpmjetpacktrace15644-59.13 %
complexnpmjetpackdeps9896-74.15 %
complexnpmbaseline 38282 

Maintenance status

Active: Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.

Author: FormidableLabs
Source Code: https://github.com/FormidableLabs/serverless-jetpack 
License: MIT license

#serverless #javascript #jetpack 

A Faster JavaScript Packager for Serverless Applications
Jacob Banks

Jacob Banks

1654502018

Android Studio Tutorial | Create a Bottom Sheet with Jetpack Compose

How to Create a Bottom Sheet With Jetpack Compose - Android Studio Tutorial

Bottom sheets are UI sheets that the user can drag up and down to show or reveal UI components. Very useful!

Get the source code for this video here:
https://github.com/philipplackner/BottomSheetCompose 

#android #jetpack 

Android Studio Tutorial | Create a Bottom Sheet with Jetpack Compose
Diego  Elizondo

Diego Elizondo

1654265520

Obtener La Dirección De La Billetera Usando WalletConnect En Android

El crecimiento de la tecnología Blockchain ha sido tan rápido que incluso aquellos que no han oído hablar de las criptomonedas o conocen su funcionamiento buscan invertir y explorar este campo. Según un estudio realizado por The Independent, el 11% de los británicos encuestados dijeron que estarían interesados ​​en las criptomonedas o la tecnología blockchain si se familiarizaran más con ella. A medida que el rumor en torno a las criptomonedas continúa extendiéndose por todo el mundo y más empresas emergentes desarrollan nuevas formas de utilizar la tecnología blockchain todos los días, no es de extrañar que más personas estén interesadas en invertir en este espacio.

En este breve artículo, aprenderemos cómo conectar una billetera criptográfica a nuestra aplicación de Android usando Jetpack Compose y WalletConnect.

¿Qué son las CryptoWallets?

Cuando hablamos de criptomonedas, hay algo muy importante llamado CryptoWallet. CrypoWallets están diseñados para almacenar activos digitales y validar transacciones. Una billetera guarda información secreta utilizando una clave privada utilizada para firmar transacciones. Las billeteras criptográficas son una forma segura de almacenar y administrar sus monedas y tokens, pero existen algunos tipos diferentes de billeteras en el mercado:

  1. Billeteras populares
    — Billeteras de escritorio
    — Billeteras web
    — Billeteras móviles
  2. Carteras frías
    - Cartera de hardware
    - Cartera de papel

¿Qué es Wallet Connect?

WalletConnect es un protocolo abierto para comunicarse de forma segura entre Wallets y Dapps (Web3 Apps). El protocolo establece una conexión remota entre dos aplicaciones y/o dispositivos utilizando un servidor Bridge para transmitir cargas útiles. Estas cargas útiles se cifran simétricamente a través de una clave compartida entre los dos pares.

La conexión la inicia un compañero que muestra un código QR o un enlace profundo con un URI estándar de WalletConnect y se establece cuando la contraparte aprueba esta solicitud de conexión. También incluye un servidor Push opcional para permitir que las aplicaciones nativas notifiquen al usuario las cargas útiles entrantes para las conexiones establecidas.

¿Qué es Jetpack Compose?

Jetpack Compose es una herramienta moderna de Android para crear interfaces de usuario nativas desarrollada por Google Developers. Jetpack Compose se compara con SwiftUI utilizado en el desarrollo de aplicaciones iOS. Esta analogía se basa en un diseño más sencillo, menos código y más lógica comercial.

Jetpack Compose simplifica el desarrollo de la interfaz de usuario en las aplicaciones de Android y permite un desarrollo de aplicaciones más rápido. El lema de Jetpack Compose es menos código, herramientas potentes, API Kotlin intuitivas y desarrollo rápido de aplicaciones.

Conectar Wallet a la aplicación de Android

En esta sección, vamos a conectar Wallet a nuestra aplicación de Android y obtener la dirección de usuario mediante Wallet Connect SDK .

Dependencias de configuración

Dentro build.gradledel archivo (Módulo) debe agregar las siguientes implementaciones.

    // Wallet Connect
    implementation 'com.github.WalletConnect:kotlin-walletconnect-lib:0.9.6'
    implementation "com.github.komputing:khex:1.0.0-RC6"
    implementation "org.java-websocket:Java-WebSocket:1.4.0"
    implementation 'com.squareup.moshi:moshi:1.8.0'
    implementation "com.squareup.okhttp3:okhttp:4.9.1"

    // Hilt
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-compiler:2.38.1"
    implementation "androidx.hilt:hilt-navigation-compose:1.0.0-beta01"

Después de esto, agregue kaptcomplementos de empuñadura en la parte superior del build.gradlearchivo.

plugins {
    // Other plugins...
    
    id 'dagger.hilt.android.plugin'
    id 'kotlin-kapt'
}

Agregar classpath al build.gradlearchivo (Proyecto)


    dependencies {
        // Hilt
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
    }

Y no olvide agregar la siguiente línea a su build.gradlearchivo. Si está utilizando Android Studio Canary, debe agregar esta línea a su settings.gradlearchivo. Luego haga clic en el botón sincronizar ahora.

maven { url 'https://jitpack.io' }

Configuración del módulo de empuñadura

Para conectar WalletConnect, necesitamos un archivo y un par de objetos de red. Para crear estos objetos vamos a utilizar Inyección de Dependencia. El siguiente código es nuestro módulo Wallet Connect.

@InstallIn(SingletonComponent::class)
@Module
object WalletConnectModule {

    @Provides
    @Singleton
    fun provideMoshi(): Moshi = Moshi.Builder().build()

    @Provides
    @Singleton
    fun provideOkhttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideSessionStorage(
        @ApplicationContext context: Context,
        moshi: Moshi,
    ): WCSessionStore = FileWCSessionStore(File(context.cacheDir, "session_store.json").apply { createNewFile() },
        moshi)
}

Configurar BirdgeServer

Para conectar nuestra aplicación a Wallet, necesitamos un Servidor Socket. Aquí hay un código sobre cómo generar este servidor.

class BridgeServer(moshi: Moshi) : WebSocketServer(InetSocketAddress(PORT)) {

    private val adapter = moshi.adapter<Map<String, String>>(
            Types.newParameterizedType(
                    Map::class.java,
                    String::class.java,
                    String::class.java
            )
    )

    private val pubs: MutableMap<String, MutableList<WeakReference<WebSocket>>> = ConcurrentHashMap()
    private val pubsLock = Any()
    private val pubsCache: MutableMap<String, String?> = ConcurrentHashMap()

    override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {
        Log.d("#####", "onOpen: ${conn?.remoteSocketAddress?.address?.hostAddress}")
    }

    override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {
        Log.d("#####", "onClose: ${conn?.remoteSocketAddress?.address?.hostAddress}")
        conn?.let { cleanUpSocket(it) }
    }

    override fun onMessage(conn: WebSocket?, message: String?) {
        Log.d("#####", "Message: $message")
        try {
            conn ?: error("Unknown socket")
            message?.also {
                val msg = adapter.fromJson(it) ?: error("Invalid message")
                val type: String = msg["type"] ?: error("Type not found")
                val topic: String = msg["topic"] ?: error("Topic not found")
                when (type) {
                    "pub" -> {
                        var sendMessage = false
                        pubs[topic]?.forEach { r ->
                            r.get()?.apply {
                                send(message)
                                sendMessage = true
                            }
                        }
                        if (!sendMessage) {
                            Log.d("#####", "Cache message: $message")
                            pubsCache[topic] = message
                        }
                    }
                    "sub" -> {
                        pubs.getOrPut(topic, { mutableListOf() }).add(WeakReference(conn))
                        pubsCache[topic]?.let { cached ->
                            Log.d("#####", "Send cached: $cached")
                            conn.send(cached)
                        }
                    }
                    else -> error("Unknown type")
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onStart() {
        Log.d("#####", "Server started")
        connectionLostTimeout = 0
    }

    override fun onError(conn: WebSocket?, ex: Exception?) {
        Log.d("#####", "onError")
        ex?.printStackTrace()
        conn?.let { cleanUpSocket(it) }
    }

    private fun cleanUpSocket(conn: WebSocket) {
        synchronized(pubsLock) {
            pubs.forEach {
                it.value.removeAll { r -> r.get().let { v -> v == null || v == conn } }
            }
        }
    }

    companion object {
        val PORT = 5000 + Random().nextInt(60000)
    }
}

Crear instancia de Wallet Connect

Cree WalletConnectViewModele inyecte Moshi, OkHttpClient y WCSessionStore. Necesitamos un par de elementos BridgeServer, Session.Callback, Session, SessionConfig. Genere esos objetos y escriba las funciones necesarias.

@HiltViewModel
class ConnectWalletViewModel @Inject constructor(
    private val moshi: Moshi,
    private val client: OkHttpClient,
    private val storage: WCSessionStore,
): ViewModel() {

    var userWallet = MutableStateFlow("")
    private set

    private var config: Session.Config? = null
    private var session: Session? = null
    private var bridge: BridgeServer? = null
    private var activeCallback: Session.Callback? = null

    init {
        initBridge()
    }

    private fun initBridge() {
        bridge = BridgeServer(moshi).apply {
            onStart()
        }
    }

    private fun resetSession() {
        session?.clearCallbacks()
        val key = ByteArray(32).also { Random().nextBytes(it) }.toHexString(prefix = "")
        config =
            Session.Config(UUID.randomUUID().toString(), "https://bridge.walletconnect.org", key)
        session = WCSession(
            config ?: return,
            MoshiPayloadAdapter(moshi),
            storage,
            OkHttpTransport.Builder(client, moshi),
            Session.PeerMeta(name = "Example App")
        )
        session?.offer()
    }


    fun connectWallet(context: Context) {
        resetSession()
        activeCallback = object : Session.Callback {
            override fun onMethodCall(call: Session.MethodCall) = Unit

            override fun onStatus(status: Session.Status) {
                status.handleStatus()
            }
        }
        session?.addCallback(activeCallback ?: return)
        context.startActivity(Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse(config?.toWCUri() ?: return)
        })
    }

    fun Session.Status.handleStatus() {
        when (this) {
            Session.Status.Approved -> sessionApproved()
            Session.Status.Closed -> sessionClosed()
            Session.Status.Connected,
            Session.Status.Disconnected,
            is Session.Status.Error,
            -> Log.e("WC Session Status", "handleStatus: $this", )
        }
    }

    private fun sessionApproved() {
        val address = session?.approvedAccounts()?.firstOrNull() ?: return
        /* Provider name*/
        // val walletType = session?.peerMeta()?.name ?: ""
        userWallet.value = address
    }

    private fun sessionClosed() {

    }
}

¡Y eso es! Cuando el usuario hace clic en cualquier botón, llama a connectWalletla función. Si una conexión es un éxito sessionApproved, se llamará a la función y se actualizará la interfaz de usuario.

Conclusión

En este artículo, analizamos cómo obtener una dirección de billetera criptográfica desde la aplicación de Android. Para hacer esto, usamos WalletConnect SDK. Para obtener más información, consulte la aplicación de muestra oficial y el código de muestra .

Enlaces útiles

Esta historia se publicó originalmente en https://betterprogramming.pub/how-to-get-wallet-address-using-walletconnect-on-android-cdffda84cda1

#wallet  #android  #jetpackcompose  #jetpack  #walletconnect 

 Obtener La Dirección De La Billetera Usando WalletConnect En Android

AndroidでWalletConnectを使用してウォレットアドレスを取得する

ブロックチェーンテクノロジーの成長は非常に急速であるため、暗号通貨について聞いたことがない、またはその仕事について知らない人でも、この分野に投資して探索しようとしています。The Independentが実施した調査によると、調査対象の英国人の11%が、暗号通貨やブロックチェーンテクノロジーに慣れれば、それに興味があると答えています。暗号通貨に関する話題が世界中に広がり続け、より多くのスタートアップがブロックチェーンテクノロジーを使用する新しい方法を毎日開発しているので、より多くの人々がこの分野への投資に興味を持っているのも不思議ではありません。

この短い記事では、JetpackComposeとWalletConnectを使用して暗号ウォレットをAndroidアプリに接続する方法を学習します。

CryptoWalletsとは何ですか?

私たちが暗号通貨について話すとき、CryptoWalletと呼ばれる最も重要なものがあります。CrypoWalletsは、デジタル資産を保存し、トランザクションを検証するように設計されています。ウォレットは、トランザクションの署名に使用される秘密鍵を使用して秘密情報を保持します。暗号通貨ウォレットは、コインやトークンを安全に保管および管理する方法ですが、市場にはいくつかの異なるタイプのウォレットがあります。

  1. ホットウォレット
    —デスクトップウォレット
    — Webウォレット
    —モバイルウォレット
  2. コールドウォレット
    —ハードウェアウォレット
    —ペーパーウォレット

WalletConnectとは何ですか?

WalletConnectは、WalletとDapps(Web3 Apps)の間で安全に通信するためのオープンプロトコルです。プロトコルは、ペイロードを中継するためにBridgeサーバーを使用して、2つのアプリやデバイス間のリモート接続を確立します。これらのペイロードは、2つのピア間の共有キーを介して対称的に暗号化されます。

接続は、QRコードまたは標準のWalletConnect URIを使用したディープリンクを表示する1つのピアによって開始され、カウンターパーティがこの接続要求を承認したときに確立されます。また、オプションのプッシュサーバーが含まれており、ネイティブアプリケーションが確立された接続の着信ペイロードをユーザーに通知できるようにします。

Jetpack Composeとは何ですか?

Jetpack Composeは、GoogleDevelopersによって開発されたネイティブUIを作成するためのAndroidモダンツールです。Jetpack Composeは、iOSアプリ開発で使用されるSwiftUIに例えられます。この例えは、より簡単な設計、より少ないコード、およびより多くのビジネスロジックに基づいています。

Jetpack Composeは、AndroidアプリでのUI開発を簡素化し、アプリ開発を高速化します。Jetpack Composeのモットーは、コードの削減、強力なツール、直感的なKotlin API、および迅速なアプリケーション開発です。

ウォレットをAndroidアプリに接続する

このセクションでは、WalletをAndroidアプリに接続し、WalletConnectSDKを使用してユーザーアドレスを取得します

依存関係の設定

(モジュール)ファイル内build.gradleに、以下の実装を追加する必要があります。

    // Wallet Connect
    implementation 'com.github.WalletConnect:kotlin-walletconnect-lib:0.9.6'
    implementation "com.github.komputing:khex:1.0.0-RC6"
    implementation "org.java-websocket:Java-WebSocket:1.4.0"
    implementation 'com.squareup.moshi:moshi:1.8.0'
    implementation "com.squareup.okhttp3:okhttp:4.9.1"

    // Hilt
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-compiler:2.38.1"
    implementation "androidx.hilt:hilt-navigation-compose:1.0.0-beta01"

この後、プラグインをファイルの先頭に追加kaptしてhiltしbuild.gradleます。

plugins {
    // Other plugins...
    
    id 'dagger.hilt.android.plugin'
    id 'kotlin-kapt'
}

build.gradle(プロジェクト)ファイルにクラスパスを追加する


    dependencies {
        // Hilt
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
    }

build.gradleそして、ファイルに以下の行を追加することを忘れないでください。Android Studio Canaryを使用している場合は、この行をsettings.gradleファイルに追加する必要があります。次に、[今すぐ同期]ボタンをクリックします。

maven { url 'https://jitpack.io' }

ヒルトモジュールのセットアップ

接続WalletConnectするには、ファイルといくつかのネットワークオブジェクトが必要です。これらのオブジェクトを作成するには、依存性注入を使用します。以下のコードは、WalletConnectモジュールです。

@InstallIn(SingletonComponent::class)
@Module
object WalletConnectModule {

    @Provides
    @Singleton
    fun provideMoshi(): Moshi = Moshi.Builder().build()

    @Provides
    @Singleton
    fun provideOkhttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideSessionStorage(
        @ApplicationContext context: Context,
        moshi: Moshi,
    ): WCSessionStore = FileWCSessionStore(File(context.cacheDir, "session_store.json").apply { createNewFile() },
        moshi)
}

BirdgeServerをセットアップする

アプリをウォレットに接続するには、ソケットサーバーが必要です。このサーバーを生成する方法のコードは次のとおりです。

class BridgeServer(moshi: Moshi) : WebSocketServer(InetSocketAddress(PORT)) {

    private val adapter = moshi.adapter<Map<String, String>>(
            Types.newParameterizedType(
                    Map::class.java,
                    String::class.java,
                    String::class.java
            )
    )

    private val pubs: MutableMap<String, MutableList<WeakReference<WebSocket>>> = ConcurrentHashMap()
    private val pubsLock = Any()
    private val pubsCache: MutableMap<String, String?> = ConcurrentHashMap()

    override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {
        Log.d("#####", "onOpen: ${conn?.remoteSocketAddress?.address?.hostAddress}")
    }

    override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {
        Log.d("#####", "onClose: ${conn?.remoteSocketAddress?.address?.hostAddress}")
        conn?.let { cleanUpSocket(it) }
    }

    override fun onMessage(conn: WebSocket?, message: String?) {
        Log.d("#####", "Message: $message")
        try {
            conn ?: error("Unknown socket")
            message?.also {
                val msg = adapter.fromJson(it) ?: error("Invalid message")
                val type: String = msg["type"] ?: error("Type not found")
                val topic: String = msg["topic"] ?: error("Topic not found")
                when (type) {
                    "pub" -> {
                        var sendMessage = false
                        pubs[topic]?.forEach { r ->
                            r.get()?.apply {
                                send(message)
                                sendMessage = true
                            }
                        }
                        if (!sendMessage) {
                            Log.d("#####", "Cache message: $message")
                            pubsCache[topic] = message
                        }
                    }
                    "sub" -> {
                        pubs.getOrPut(topic, { mutableListOf() }).add(WeakReference(conn))
                        pubsCache[topic]?.let { cached ->
                            Log.d("#####", "Send cached: $cached")
                            conn.send(cached)
                        }
                    }
                    else -> error("Unknown type")
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onStart() {
        Log.d("#####", "Server started")
        connectionLostTimeout = 0
    }

    override fun onError(conn: WebSocket?, ex: Exception?) {
        Log.d("#####", "onError")
        ex?.printStackTrace()
        conn?.let { cleanUpSocket(it) }
    }

    private fun cleanUpSocket(conn: WebSocket) {
        synchronized(pubsLock) {
            pubs.forEach {
                it.value.removeAll { r -> r.get().let { v -> v == null || v == conn } }
            }
        }
    }

    companion object {
        val PORT = 5000 + Random().nextInt(60000)
    }
}

WalletConnectインスタンスを作成する

WalletConnectViewModelMoshi、OkHttpClient、およびWCSessionStoreを作成して挿入します。いくつかのアイテムが必要ですBridgeServer、、Session.Callbackセッション、SessionConfig。そのオブジェクトを生成し、必要な関数を記述します。

@HiltViewModel
class ConnectWalletViewModel @Inject constructor(
    private val moshi: Moshi,
    private val client: OkHttpClient,
    private val storage: WCSessionStore,
): ViewModel() {

    var userWallet = MutableStateFlow("")
    private set

    private var config: Session.Config? = null
    private var session: Session? = null
    private var bridge: BridgeServer? = null
    private var activeCallback: Session.Callback? = null

    init {
        initBridge()
    }

    private fun initBridge() {
        bridge = BridgeServer(moshi).apply {
            onStart()
        }
    }

    private fun resetSession() {
        session?.clearCallbacks()
        val key = ByteArray(32).also { Random().nextBytes(it) }.toHexString(prefix = "")
        config =
            Session.Config(UUID.randomUUID().toString(), "https://bridge.walletconnect.org", key)
        session = WCSession(
            config ?: return,
            MoshiPayloadAdapter(moshi),
            storage,
            OkHttpTransport.Builder(client, moshi),
            Session.PeerMeta(name = "Example App")
        )
        session?.offer()
    }


    fun connectWallet(context: Context) {
        resetSession()
        activeCallback = object : Session.Callback {
            override fun onMethodCall(call: Session.MethodCall) = Unit

            override fun onStatus(status: Session.Status) {
                status.handleStatus()
            }
        }
        session?.addCallback(activeCallback ?: return)
        context.startActivity(Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse(config?.toWCUri() ?: return)
        })
    }

    fun Session.Status.handleStatus() {
        when (this) {
            Session.Status.Approved -> sessionApproved()
            Session.Status.Closed -> sessionClosed()
            Session.Status.Connected,
            Session.Status.Disconnected,
            is Session.Status.Error,
            -> Log.e("WC Session Status", "handleStatus: $this", )
        }
    }

    private fun sessionApproved() {
        val address = session?.approvedAccounts()?.firstOrNull() ?: return
        /* Provider name*/
        // val walletType = session?.peerMeta()?.name ?: ""
        userWallet.value = address
    }

    private fun sessionClosed() {

    }
}

以上です!ユーザーがいずれかのボタンをクリックしたら、connectWallet関数を呼び出します。接続が成功した場合、sessionApproved関数が呼び出され、UIが更新されます。

結論

この記事では、Androidアプリから暗号ウォレットアドレスを取得する方法について説明しました。これを行うために、WalletConnectSDKを使用しました。詳細については、公式のサンプルアプリサンプルコードを参照してください。

便利なリンク

このストーリーは、もともとhttps://betterprogramming.pub/how-to-get-wallet-address-using-walletconnect-on-android-cdffda84cda1で公開されました

#wallet #android #jetpackcompose #jetpack #WalletConnect

AndroidでWalletConnectを使用してウォレットアドレスを取得する
Jade Bird

Jade Bird

1654152684

Android Studio Tutorial | Navigation Drawer with Jetpack Compose

How to Create a Navigation Drawer With Jetpack Compose - Android Studio Tutorial

A navigation drawer is a slidable side menu you can use to show and reveal information like menu items. In this video you'll learn how to implement that.

Get the source code for this video here:
https://github.com/philipplackner/NavigationDrawerCompose 


Navigation Drawer Using Jetpack compose

What Is Navigation Drawer?

Navigation Drawer is a sliding left menu that is used to display the menu content in Android.
It makes it easy to navigate between different screens.
The drawer appears when the user touches the drawer menu icon in the app bar.
It also appears when the user swipes a finger from the left edge of the screen.

Navigation Drawer using jetpack compose have same features and functionality.
To add a drawer in jetpack compose we use the Scaffold component.
Scaffold implements the basic material design visual layout structure.
It provides APIs to put together several material components to construct your screen.

In this article, we make a sample Navigation Drawer Using Jetpack compose.

We will take some static data to show in the drawer menu.

Lat’s take an example to check how the navigation drawer works in jetpack compose.
We have added all code in the MainActivity file. You can do it according to the need and architecture of the application.

Let’s look at the code.

Function To Add Heading In Drawer :

@Composable
fun AddDrawerHeader(
    title: String,
    titleColor: Color = Color.Black,
) {
    Card(
        elevation = 4.dp,
        modifier = Modifier
            .fillMaxWidth(),
        border = BorderStroke(1.dp, color = Color.Gray),
 
 
        ) {
        Text(
            text = title,
            style = TextStyle(
                fontWeight = FontWeight.Bold,
                fontSize = 16.sp,
                color = titleColor
            ),
            modifier = Modifier.padding(14.dp)
        )
 
    }
}

Function To Add Content/Sub-Content In Drawer :

@Composable
fun DrawerView() {
    val language = listOf("English ", "Hindi", "Arabic")
    val category = listOf("Cloth", "electronics", "fashion","Food")
    LazyColumn {
        item {
            AddDrawerHeader(title = "Language")
        }
        items(language.size){index->
 
            AddDrawerContentView(title = language[index], selected = if (index==1)true else false)
        }
        item {
            AddDrawerHeader(title = "Category")
        }
 
        items(category.size){index->
 
            AddDrawerContentView(title = category[index], selected = if (index==2)true else false)
        }
    }
 
}
@Composable
fun AddDrawerContentView(title: String,  selected: Boolean) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .clickable {}
            .padding(horizontal = 16.dp, vertical = 12.dp),
 
 
        ) {
 
        if (title.isNotEmpty()) {
            if (selected)
                Text(text = title, modifier = Modifier.weight(1f), color = Color.Black,style = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 14.sp,
                    color = Color.Black
                ))
            else
                Text(text = title, modifier = Modifier.weight(1f),fontSize = 12.sp)
        }
 
    }
}

Complete Code :

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val scaffoldState = rememberScaffoldState()
            val scope = rememberCoroutineScope()
            DrawersampleTheme {
 
                Scaffold(
                    scaffoldState = scaffoldState,
                    topBar = {
                        TopAppBar(
                            title = {
                                Text(text = "Drawer Sample")
                            },
                            navigationIcon = {
                                IconButton(
                                    onClick = {
 
                                        scope.launch {
                                            scaffoldState.drawerState.open()
                                        }
                                    },
                                ) {
                                    Icon(
                                        Icons.Rounded.Menu,
                                        contentDescription = ""
                                    )
                                }
                            })
                    },
 
 
                    drawerContent = { DrawerView() },
 
                    bottomBar = {}//add bottom navigation content
 
                ) {
 
                }
            }
        }
    }
}
 
 
@Composable
fun DrawerView() {
    val language = listOf("English ", "Hindi", "Arabic")
    val category = listOf("Cloth", "electronics", "fashion","Food")
    LazyColumn {
        item {
            AddDrawerHeader(title = "Language")
        }
        items(language.size){index->
 
            AddDrawerContentView(title = language[index], selected = if (index==1)true else false)
        }
        item {
            AddDrawerHeader(title = "Category")
        }
 
        items(category.size){index->
 
            AddDrawerContentView(title = category[index], selected = if (index==2)true else false)
        }
    }
 
}
@Composable
fun AddDrawerContentView(title: String,  selected: Boolean) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .clickable {}
            .padding(horizontal = 16.dp, vertical = 12.dp),
 
 
        ) {
 
        if (title.isNotEmpty()) {
            if (selected)
                Text(text = title, modifier = Modifier.weight(1f), color = Color.Black,style = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 14.sp,
                    color = Color.Black
                ))
            else
                Text(text = title, modifier = Modifier.weight(1f),fontSize = 12.sp)
        }
 
    }
}
@Composable
fun AddDrawerHeader(
    title: String,
    titleColor: Color = Color.Black,
) {
    Card(
        elevation = 4.dp,
        modifier = Modifier
            .fillMaxWidth(),
        border = BorderStroke(1.dp, color = Color.Gray),
 
 
        ) {
        Text(
            text = title,
            style = TextStyle(
                fontWeight = FontWeight.Bold,
                fontSize = 16.sp,
                color = titleColor
            ),
            modifier = Modifier.padding(14.dp)
        )
 
    }
}

Now, Run the code and check the result.

Result :

Conclusion :

In this article, we learn how we can add a drawer to our app using jetpack compose.
For more information about the drawer in jetpack compose, please follow the link.

#jetpack #android 

Android Studio Tutorial | Navigation Drawer with Jetpack Compose

Build a Jetpack Compose App with Material Design 3

How to Build Stunning Material 3 Apps with Jetpack Compose - Android Studio Tutorial

In this video you'll learn to properly build a Jetpack Compose app with Google's new Material 3 design standard. You'll learn about color theory, typography and shapes as well as using the Material 3 composables in your app.

Get the source code for this video here:
https://github.com/philipplackner/Material3App 

#jetpack #android 

Build a Jetpack Compose App with Material Design 3

Introducción A Los Modificadores De Redacción De Jetpack

Este artículo supone que el lector tiene conocimientos básicos sobre cómo funciona Jetpack Compose, si no está familiarizado con las @Composablefunciones anotadas, le aconsejo que estudie estos temas antes de leer este artículo.

Jetpack Compose es una biblioteca diseñada para traer patrones de IU declarativos al desarrollo de Android.

Es una función innovadora que cambia por completo la forma en que los desarrolladores tienen que pensar y diseñar su interfaz de usuario, dado que hay poco que ver con el (antiguo) sistema de vista XML.

Los Componibles predeterminados (basados ​​en la especificación Material UI) son altamente personalizables de forma predeterminada, pero la existencia de Modificadores aumenta las posibilidades de personalización a más de nueve mil.

Personalización de un componente regular

La mayoría de Composables son personalizables a través de sus parámetros, por ejemplo, podemos cambiar el color y el progreso actual de un CircularProgressBar(), o establecer íconos iniciales/posteriores para TextField()Composables.

Pero en la mayoría de los casos, esto puede no ser suficiente. Las cajas, por ejemplo, no tienen muchas opciones de personalización a través de sus parámetros.

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
)

Gist 1: Parámetros disponibles de Box Composable

Composable Box()no parece un componente muy flexible, dado que no tenemos parámetros explícitos para modificar los colores, la forma, los bordes, la altura, el ancho y muchas otras propiedades útiles para lograr el objetivo de la interfaz de usuario.

Pero la cuestión es que no necesitamos todos estos parámetros simplemente por la existencia de modificadores.

¿Qué es un modificador?

De acuerdo con la documentación de Android, un Modificador es un conjunto encadenado de propiedades que se pueden asignar a un Composable, capaz de modificar el diseño, agregar detectores de eventos y mucho más.

Imagen 1: Algunos de los modificadores disponibles para Box Composable dentro de un Scaffold.

Los modificadores se encadenan usando un patrón de fábrica, por lo que si deseamos establecer el color de fondo del cuadro en amarillo y establecer su altura y ancho, podríamos hacer algo como esto:


Box(
     modifier = Modifier
         .background(Color.Yellow)
         .height(60.dp)
         .width(100.dp)
 )

Gist 2: Colorear una caja y establecer sus dimensiones

Imagen 2: Resultado del código diseñado en Gist 2.

A veces es una cuestión de orden

Al realizar cambios menores en un Composable, el orden de los Modificadores puede no hacer ninguna diferencia, pero cuando se necesita un diseño más complejo, debemos prestar atención al orden de nuestros Modificadores encadenados. Para ejemplificar este uso, codificaremos el diseño a continuación:

Primero, vamos a crear la estructura base de nuestro diseño, con un Scaffold como el "esqueleto" de la pantalla y una caja para envolver nuestro contenido. Tenga en cuenta que ya estamos usando un modificador aquí Modifier.fillMaxSize()para hacer que Box Composable se ajuste al tamaño completo de Scaffold.

Scaffold {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // Our content will be coded here 
    }
}

Ahora agreguemos nuestros componibles, modificadores y entendamos lo que está pasando aquí:

 Scaffold {
     Box(
         modifier = Modifier.fillMaxSize(),
         contentAlignment = Alignment.Center
     ) {
         Column(
             modifier = Modifier
                 .clip(CircleShape)
                 .fillMaxWidth(0.8f)
                 .aspectRatio(1f)
                 .background(Color.Gray),
             horizontalAlignment = Alignment.CenterHorizontally,
             verticalArrangement = Arrangement.Center
         ) {
             Text("I am text")
             Text("I am text too")
         }
     }
 }

Para lograr este diseño, decidí usar un cuadro como envoltorio de pantalla con el modificador fillMaxSize(), por lo que este mismo componible llenará todo el tamaño de su padre. Como complemento a la caja, utilicé una Columna para que fuera nuestro círculo con los elementos componibles de Texto dentro.

En este orden exacto Modifier, podemos lograr fácilmente el círculo gris con dos textos (uno encima del otro), ahora entendamos qué hace cada uno de los modificadores de la Columna y luego cambiemos su orden para causar que el diseño se rompa a propósito.

  • clip: El modificador de recorte sirve para recortar el Composable para que podamos cambiar fácilmente su forma. Usando este modificador podemos transformar un Box rectangular (la forma predeterminada del Box Composable) en un rectángulo de esquina redondeada o un círculo, por ejemplo.
  • fillMaxWidth: este modificador sirve para informar al componible para que se ajuste al ancho máximo del padre (esto es similar a una vista de ancho de 0dp restringida al inicio y al final de su padre cuando se usa Diseño de restricción). El parámetro de fracción sirve para definir el "peso" de este autorrelleno, por ejemplo, al usar 0.1f llenaremos el 10% del ancho del padre y 1.0f llenará todo el ancho disponible.
  • aspectRatio: Este modificador intentará igualar la altura y el ancho de un componible de acuerdo con el parámetro de proporción. Al usar clip , definimos nuestro componible como un círculo y fillMaxWidth establece que el diámetro de nuestro círculo llenará el 80% del ancho máximo del padre, pero esta solución no dará como resultado un círculo perfecto, al eliminar aspectRatio podemos ver que la columna tiene la forma de un rectángulo de esquinas redondeadas y no un círculo. Esto sucede porque el círculo necesita que la altura y el ancho retengan el mismo valor, por lo que al usar aspectRatio(1)podemos asegurarnos de que nuestro círculo tenga el mismo ancho y alto.

  • backgroundColor: Establece el color de fondo de un componible al valor dado. El backgroundColor es uno de los modificadores en los que importa el orden, y podemos ver este comportamiento si nos movemos backgroundColorantes del clipmodificador. Esto sucede porque esencialmente estamos pintando el componible antes de recortar sus límites, por cuestiones prácticas, nuestra Columna se recorta para ser un Círculo. Pero debido a que lo pintamos antes del recorte, parece que el clipmodificador no funciona, pero lo hace, es solo una cuestión de orden.
Column(
    modifier = Modifier
        .background(Color.Gray)
        .clip(CircleShape)
        .fillMaxWidth(0.8f)
        .aspectRatio(1f),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) 

Modificadores contenidos en el alcance

La biblioteca Compose Standard ofrece toneladas de modificadores de alcance comunes, en otras palabras, modificadores que están disponibles de forma predeterminada, por lo tanto, independientes del alcance actual.

Pero cada vez que se usan Composables como Columna, Fila y Cuadro, probablemente haya notado que la contentlambda está envuelta dentro de un ámbito, por ejemplo, en las columnas que tenemos ColumnScope:

ColumnScopees una interfaz responsable de establecer la altura de Composable en relación con el tamaño de su contenido (envoltura de contenido de forma predeterminada), y también proporciona un conjunto limitado de modificadores que solo están disponibles dentro de este ámbito: weight, aligny alignBy, esto es lo que yo llamo ámbito- Modificadores retenidos.

Puede leer más sobre los ámbitos y sus respectivos modificadores retenidos accediendo al código fuente a través del IDE o leyendo la documentación oficial.

Si volvemos a nuestro diseño de columna en forma de círculo y agregamos el alignmodificador a nuestros TextComposables, podemos alinearlos horizontalmente en la columna

Estos modificadores de alineación también están disponibles en Row Composable, pero en este caso, son los encargados de alinear el contenido verticalmente dentro de la fila. Esto también es cierto para BoxScope, pero en este caso tenemos modificadores que pueden controlar la posición tanto horizontal como verticalmente.

 Column(
     modifier = Modifier
         .clip(CircleShape)
         .background(Color.Gray)
         .fillMaxWidth(0.8f)
         .aspectRatio(1f),
     horizontalAlignment = Alignment.CenterHorizontally,
     verticalArrangement = Arrangement.Center
 ) {
     Text("I am text", modifier = Modifier.align(Alignment.End))
     Text("I am text too", modifier = Modifier.align(Alignment.Start))
 }

Los cambios anteriores producirán el diseño a continuación, al agregar los alignmodificadores a nuestros Textcomponibles, tenemos control directo de su posición horizontal dentro de la Columna.

Modificadores de recombinación

Los modificadores también se pueden recombinar mediante la creación de un solo modificador que aplique otros modificadores, en nuestro caso, podemos crear uno makeColouredCircleque reciba un parámetro de color.


fun Modifier.makeColouredCircle(color: Color): Modifier =
    this.then(
        Modifier
            .clip(CircleShape)
            .background(color)
            .fillMaxWidth(0.8f)
            .aspectRatio(1f)
    )

Dado que los modificadores se encadenan uno tras otro, debemos garantizar que los agregados previamente no se perderán al aplicar nuestro modificador personalizado, es por eso que debemos usar la this.thendeclaración. La thenfunción sirve para agregar más modificadores a una instancia de modificador preexistente.

En este ejemplo, estamos agregando modificadores de medición en nuestro Modificador personalizado, pero tenga en cuenta que NO ES una buena práctica, por lo general, fillMaxWidth y aspectRatio debe usarse directamente en nuestro Column Composable

No tiene que preocuparse por tener en cuenta esta regla cada vez que cree un nuevo Modificador personalizado, si simplemente intenta devolver una nueva instancia de Modificador, el IDE le mostrará un error:

Ahora podemos usar nuestro Modificador personalizado en nuestra Columna y lograr el mismo resultado que teníamos antes. Tenga en cuenta que ahora podemos cambiar dinámicamente el color de nuestro Círculo:

Column(
    modifier = Modifier.makeColouredCircle(Color.Green),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) {
    Text("I am text")
    Text("I am text too")
}

Modificadores de interacción

Los modificadores también se pueden usar para agregar interacciones a nuestros Componibles, como hacer que se pueda hacer clic, arrastrar y muchos más. Para ejemplificar esta posibilidad, usaremos un caso de uso muy común: una lista de tarjetas en las que se puede hacer clic que conduce a una página de detalles.

Sugerencia de buenas prácticas: cada vez que cree un Composable, defina un parámetro de Modificador con un Modificador vacío predeterminado. De esta forma podemos flexibilizar nuestros Composables.

@Composable
fun Card(
    modifier: Modifier = Modifier,
    text: String
) {
    Row(
        modifier = modifier
            .border(
                BorderStroke(1.5.dp, color = Color.Black),
                shape = RoundedCornerShape(12.dp)
            )
            .padding(10.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.Settings,
            contentDescription = null
        )
        Spacer(Modifier.width(10.dp))
        Text(text = text, modifier = Modifier.fillMaxWidth())
    }
}
Scaffold {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        Card(text = "Card 1", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 2", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 3", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 4", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 5", modifier = Modifier.fillMaxWidth())
    }
}

Este es nuestro diseño base:

Ahora vamos a agregar una devolución de llamada a nuestra Card Composable para que podamos definir qué sucede cuando hacemos clic en ella.

@Composable
fun Card(
    modifier: Modifier = Modifier,
    text: String,
    onClick: () -> Unit
) {
    Row(
        modifier = modifier
            .border(
                BorderStroke(1.5.dp, color = Color.Black),
                shape = RoundedCornerShape(12.dp)
            )
            .clickable {
                onClick()
            }
            .padding(10.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.Settings,
            contentDescription = null
        )
        Spacer(Modifier.width(10.dp))
        Text(text = text, modifier = Modifier.fillMaxWidth())
    }
}

Para hacer que se pueda hacer clic en nuestro componible, debemos usar el clickablemodificador, esto permite que el componente reciba eventos de clic o clic de accesibilidad.

Tenga en cuenta que simplemente estamos agregando el onClickparámetro al modificador de nuestro componible, entonces, ¿por qué no eliminar este parámetro y simplemente agregar la propiedad en la que se puede hacer clic directamente en el parámetro del modificador? Bueno, eso es cuestión de legibilidad y buenas prácticas. Al definir explícitamente el onClick parámetro, podemos afirmar que se supone que se puede hacer clic en este composable dado, sin este parámetro, no hay forma de que otros desarrolladores sepan sobre este evento de clic esperado en Card Composable porque, en teoría, cada función componible que tiene un modificador se puede hacer clic en el parámetro si agrega el clickablemodificador a su cadena de modificadores.

Ahora agreguemos un poco de comportamiento para nuestros Card Composables.

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(20.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp)
) {
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
}

¡Listo, ahora se puede hacer clic en las tarjetas!

Referencias

https://developer.android.com/jetpack/compose/modifiers

https://developer.android.com/jetpack/compose/modifiers-list

Fuente: https://betterprogramming.pub/jetpack-compose-understanding-modifiers-part-1-introduction-831582dc067d

#jetpack 

Introducción A Los Modificadores De Redacción De Jetpack
高橋  花子

高橋 花子

1653562860

JetpackComposeモディファイアの概要

この記事は、読者がJetpack Composeの動作に関する基本的な知識を持っていることを前提としています。@Composable注釈付き関数に精通していない場合は、この記事を読む前にこれらの主題を学習することをお勧めします。

Jetpack Composeは、Android開発に宣言型UIパターンをもたらすように設計されたライブラリです。

これは、(古い)XMLビューシステムとはほとんど関係がないことを考えると、開発者がUIを考えて設計する方法を完全に変えるゲームを変える機能です。

デフォルトのComposables(Material UI仕様に基づく)はデフォルトで高度にカスタマイズ可能ですが、Modifiersの存在により、カスタマイズ可能性が9000以上に増加します。

通常のコンポーネントのカスタマイズ

ほとんどのコンポーザブルは、パラメータを使用してカスタマイズできます。たとえば、の色と現在の進行状況を変更したり、コンポーザブルCircularProgressBar()の先頭/末尾のアイコンを設定したりできTextField()ます。

しかし、ほとんどの場合、これでは不十分な場合があります。たとえば、ボックスには、パラメータによる多くのカスタマイズオプションがありません。

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
)

要旨1:ボックス構成可能な利用可能なパラメータ

UIの目標を達成するために色、形状、境界線、高さ、幅、およびその他の多くの便利なプロパティを変更するための明示的なパラメーターがないため、Box()Composableは非常に柔軟なコンポーネントのようには見えません。

ただし、モディファイアが存在するという理由だけで、これらのパラメータのすべてが必要になるわけではありません。

モディファイアとは何ですか?

Androidのドキュメントによると、Modifierは、Composableに割り当てることができる一連のプロパティであり、レイアウトの変更、イベントリスナーの追加などを行うことができます。

画像1:足場内のボックスコンポーザブルで使用可能なモディファイヤの一部。

モディファイヤはファクトリパターンを使用してチェーンされているため、ボックスの背景色を黄色に設定し、その高さと幅を設定したい場合は、次のようにすることができます。


Box(
     modifier = Modifier
         .background(Color.Yellow)
         .height(60.dp)
         .width(100.dp)
 )

要旨2:ボックスの色付けと寸法の設定

画像2:Gist2で設計されたコードの結果。

時々それは秩序の問題です

コンポーザブルに小さな変更を加える場合、モディファイアの順序に違いはありませんが、より複雑なレイアウトが必要な場合は、チェーンモディファイアの順序に注意する必要があります。この使用法を例示するために、以下のレイアウトをコーディングします。

まず、画面の「スケルトン」としてScaffoldを使用し、コンテンツをラップするボックスを使用して、レイアウトの基本構造を作成します。Modifier.fillMaxSize()Box Composableを足場のサイズ全体に合わせるために、ここではすでにモディファイヤを使用していることに注意してください。

Scaffold {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // Our content will be coded here 
    }
}

次に、コンポーザブル、モディファイアを追加して、ここで何が起こっているのかを理解しましょう。

 Scaffold {
     Box(
         modifier = Modifier.fillMaxSize(),
         contentAlignment = Alignment.Center
     ) {
         Column(
             modifier = Modifier
                 .clip(CircleShape)
                 .fillMaxWidth(0.8f)
                 .aspectRatio(1f)
                 .background(Color.Gray),
             horizontalAlignment = Alignment.CenterHorizontally,
             verticalArrangement = Arrangement.Center
         ) {
             Text("I am text")
             Text("I am text too")
         }
     }
 }

このレイアウトを実現するために、修飾子付きの画面ラッパーとしてBoxを使用することにしましたfillMaxSize()。これにより、この同じコンポーザブルが親のサイズ全体を埋めることになります。ボックスを補完するものとして、列を使用して、テキストコンポーザブルを内部に持つ円を作成しました。

この正確なModifier順序で、2つのテキスト(上下に重ねて)で灰色の円を簡単に作成できます。次に、列の各修飾子の機能を理解し、それらの順序を変更して、意図的にレイアウトを壊します。

  • clip:クリップモディファイヤは、コンポーザブルをクリップするのに役立つため、その形状を簡単に変更できます。このモディファイヤを使用すると、たとえば、長方形のボックス(Box Composableのデフォルトの形状)を角の丸い長方形または円に変換できます。
  • fillMaxWidth:この修飾子は、親の最大幅に合うようにComposableに通知するのに役立ちます(これは、制約レイアウトを使用するときに親の開始と終了に制約された0dp幅のビューに似ています)。分数パラメータは、このオートフィルの「重み」を定義するのに役立ちます。たとえば、0.1fを使用すると、親の幅の10%が塗りつぶされ、1.0fは使用可能な幅全体が塗りつぶされます。
  • aspectRatio:このモディファイアは、レシオパラメータに従ってコンポーザブルの高さと幅を等しくしようとします。を使用しclip て、コンポーザブルを円と定義fillMaxWidth し、円の直径が親の最大幅の80%を満たすと述べていますが、このソリューションを削除するaspectRatio と、列が次のような形状になっていることがわかります。円ではなく、角の丸い長方形。これは、円が同じ値を保持するために高さと幅を必要とするために発生します。したがって、を使用 aspectRatio(1)することで、円の幅と高さが同じになるようにすることができます。

  • backgroundColor: Sets the background color of a composable to the given value. The backgroundColor is one of the modifiers in which order matters, and we can see this behavior if we move backgroundColor to before the clip modifier. This happens because essentially we are painting the composable before clipping its bounds, for practical matters, our Column is clipped to be a Circle. But because we painted it before the clipping it looks like the clip modifier isn’t working, but it is, it’s just a matter of order.
Column(
    modifier = Modifier
        .background(Color.Gray)
        .clip(CircleShape)
        .fillMaxWidth(0.8f)
        .aspectRatio(1f),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) 

Scope contained Modifiers

Compose Standard library delivers tons of common scope modifiers, in other words, modifiers that are available by default, therefore independent of the current scope.

ただし、Column、Row、BoxなどのComposableを使用する場合は常に、ラムダがスコープ内にラップされていることに気付いたと思います。contentたとえば、次のような列がありますColumnScope

ColumnScopeは、コンテンツサイズ(デフォルトではwrap-content)に対するComposableの高さを設定するためのインターフェイスであり、このスコープ内でのみ使用可能な限定された修飾子のセットを提供します:weight、、、alignおよびalignBy、これらは私がスコープと呼んでいるものです-保持された修飾子。

IDEからソースコードにアクセスするか、公式ドキュメントを読むことで、スコープとそれぞれの保持されている修飾子の詳細を読むことができます。

円の形をした列のレイアウトに戻りalign、コンポーザブルに修飾子を追加するTextと、列の中でそれらを水平に揃えることができます。

これらの配置修飾子は、行構成可能でも使用できますが、この場合、行の内側でコンテンツを垂直に配置する役割を果たします。これはBoxScopeにも当てはまりますが、この場合、水平方向と垂直方向の両方で位置を制御できる修飾子があります。

 Column(
     modifier = Modifier
         .clip(CircleShape)
         .background(Color.Gray)
         .fillMaxWidth(0.8f)
         .aspectRatio(1f),
     horizontalAlignment = Alignment.CenterHorizontally,
     verticalArrangement = Arrangement.Center
 ) {
     Text("I am text", modifier = Modifier.align(Alignment.End))
     Text("I am text too", modifier = Modifier.align(Alignment.Start))
 }

上記の変更により、以下のレイアウトが作成されます。コンポーザブルにalignモディファイTextヤを追加することで、列内の水平位置を直接制御できます。

修飾子の再結合

モディファイアは、他のモディファイアを適用する単一のモディファイアを作成することで再結合することもできます。この場合、makeColouredCircleカラーパラメータを受け取るを作成できます。


fun Modifier.makeColouredCircle(color: Color): Modifier =
    this.then(
        Modifier
            .clip(CircleShape)
            .background(color)
            .fillMaxWidth(0.8f)
            .aspectRatio(1f)
    )

Since Modifiers are chained one after another we have to guarantee that the ones previously added won’t be lost when applying our customized modifier, that's why we have to use this.then statement. The then function serves to append more modifiers to a pre-existing Modifier instance.

In this example, we are adding measurement modifiers in our custom Modifier but keep in mind this IS NOT a good practice, usually fillMaxWidth and aspectRatio should be used directly in our Column Composable

You don’t have to worry about keeping this rule in mind every time you create a new a custom Modifier, if you try simply returning a brand new Modifier instance the IDE will show you an error:

これで、列でカスタム修飾子を使用して、以前と同じ結果を得ることができます。これで、円の色を動的に変更できることに注意してください。

Column(
    modifier = Modifier.makeColouredCircle(Color.Green),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) {
    Text("I am text")
    Text("I am text too")
}

相互作用修飾子

モディファイアを使用して、クリック可能、ドラッグ可能など、コンポーザブルにインタラクションを追加することもできます。この可能性を例示するために、非常に一般的な使用例を使用します。詳細ページにつながるクリック可能なカードのリストです。

グッドプラクティスのヒント:Composableを作成するときはいつでも、デフォルトの空のModifierを使用してModifierパラメーターを定義します。このようにして、コンポーザブルをより柔軟にすることができます。

@Composable
fun Card(
    modifier: Modifier = Modifier,
    text: String
) {
    Row(
        modifier = modifier
            .border(
                BorderStroke(1.5.dp, color = Color.Black),
                shape = RoundedCornerShape(12.dp)
            )
            .padding(10.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.Settings,
            contentDescription = null
        )
        Spacer(Modifier.width(10.dp))
        Text(text = text, modifier = Modifier.fillMaxWidth())
    }
}
Scaffold {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        Card(text = "Card 1", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 2", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 3", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 4", modifier = Modifier.fillMaxWidth())
        Card(text = "Card 5", modifier = Modifier.fillMaxWidth())
    }
}

これが基本レイアウトです。

次に、Card Composableにコールバックを追加して、クリックしたときに何が起こるかを定義できるようにします。

@Composable
fun Card(
    modifier: Modifier = Modifier,
    text: String,
    onClick: () -> Unit
) {
    Row(
        modifier = modifier
            .border(
                BorderStroke(1.5.dp, color = Color.Black),
                shape = RoundedCornerShape(12.dp)
            )
            .clickable {
                onClick()
            }
            .padding(10.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.Settings,
            contentDescription = null
        )
        Spacer(Modifier.width(10.dp))
        Text(text = text, modifier = Modifier.fillMaxWidth())
    }
}

コンポーザブルクリック可能にするには、clickable修飾子を使用する必要があります。これにより、コンポーネントがクリックイベントまたはアクセシビリティクリックイベントを受信できるようになります。

onClickコンポーザブルのモディファイアにパラメータを追加しているだけなので、このパラメータを削除して、モディファイアパラメータに直接クリック可能なプロパティを追加してみませんか?まあ、それは読みやすさと良い習慣の問題です。パラメータを明示的に定義することによりonClick 、この特定のコンポーザブルはクリック可能であると断言できます。このパラメータがないと、理論的にはすべてのコンポーザブル関数に修飾子があるため、他の開発者がカードコンポーザブルでこの予想されるクリックイベントについて知る方法はありません。clickableモディファイヤをモディファイアチェーンに追加すると、パラメータをクリックできます。

次に、カードコンポーザブルの動作を追加しましょう。

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(20.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp)
) {
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
    Card(
        text = "Card 1",
        modifier = Modifier.fillMaxWidth(),
        onClick = {
            Log.d("card", "card clicked")
        }
    )
}

これで、カードをクリックできるようになりました。

参考文献

https://developer.android.com/jetpack/compose/modifiers

https://developer.android.com/jetpack/compose/modifiers-list

ソース:https ://betterprogramming.pub/jetpack-compose-understanding-modifiers-part-1-introduction-831582dc067d

#jetpack 

JetpackComposeモディファイアの概要
Saul  Alaniz

Saul Alaniz

1653499260

Crear Una LazyColumn Con Indexación Para Jetpack Compose

Es RecyclerViewposible que necesiten "este" y "aquel" archivo y mucha configuración en ellos, pero nadie estará en desacuerdo con que es una clase madura y muchas bibliotecas se construyen teniendo RecyclerViewen cuenta.

¡Ojalá pudiera decir lo mismo de ListViewy ListActivitypero no lo haré! ¡Basta de hablar de nuestros pecados del pasado y debemos ceñirnos al futuro porque es Jetpack Compose en punto!

Aparte de Double Header LazyColumn , realmente extrañé la RecyclerViews con indexación al costado. Pero es el momento de Jetpack Compose, por lo que se puede hacer una cosa: crear mi propia personalización LazyColumncon indexación.

El enfoque tiene dos implementaciones casi idénticas. La principal diferencia entre ellos es que la IndexedLazyColumnimplementación necesita un todo LazyColumny sus LazyListStateparámetros, pero IndexedDataLazyColumnnecesita el itemContentparámetro, desde el itemsparámetro desde LazyColumn. ¿Te sientes confundido? No se preocupe, ¡creo que al final de este artículo tendrá un buen punto de vista con la oración anterior!

 

Implementación de datos simples

A continuación puede ver la IndexedDataLazyColumnfunción sin su cuerpo.

@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
@Composable
fun <T> IndexedDataLazyColumn(
    indices: List<T>,
    data: List<T>,
    modifier: Modifier = Modifier,
    predicate: (T) -> Int,
    mainItemContent: @Composable LazyItemScope.(Int) -> Unit,
    indexedItemContent: @Composable (T, Boolean) -> Unit
)

Leyendo uno a uno los parámetros:

  • indices: Una lista simple de sus elementos de índice (por ejemplo, 1..10, A..Z, etc.).
  • data: la lista que contiene sus elementos de datos (por ejemplo, películas, nombres, fabricantes de automóviles, lista de compras, etc.).
  • modifier: Solo otro Modifiercomo el que estás usando con otros Composables.
  • predicate: Esta lambda aquí, es la conexión entre indicesy data. Puede escribir su propia lógica, pero debe tener cuidado de agregar siempre como parámetro un elemento de la indiceslista y devolver el índice de un elemento de data.
  • mainItemContent: Aquí está el Composable que mostrará el dataelemento en la pantalla.
  • indexedItemContent: Aquí está el Composable que mostrará el indiceselemento en la pantalla.

Aquí hay un ejemplo

@ExperimentalFoundationApi
@RequiresApi(Build.VERSION_CODES.N)
@Composable
fun ExampleIndexedDataLazyColumn(data: List<CustomListItem2>,
                       indices: List<Char> = ('A'..'Z').toList()) {

    IndexedDataLazyColumn(
        // The list of the indices
        indices = indices,
        // The list of the actual data
        data = data,
        // The modifier is exported for the Column, the one with the indices
        modifier = Modifier
            .background(color = Color.Transparent)
            .height(300.dp),
        // The way to connect the index with a data item (here the index item matches the first letter of the surname)
        predicate = {
            data.indexOfFirst { item ->
                item.surname.startsWith(it.toString(), true) }
        },
        // The list of the main data
        mainItemContent = {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(5.dp)
                    .clickable { },
                elevation = 10.dp
            ) {
                Column {
                    Text(text = data[it].surname,
                        fontSize = 17.sp, fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic,
                        modifier = Modifier.padding(start = 10.dp))
                    Text(text = data[it].name,
                        fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
                        modifier = Modifier.padding(start = 15.dp))
                }
            }
        },
        // The item content for the indices list
        indexedItemContent = { item, isSelected ->
            Text(
                modifier = Modifier
                    .background(color = Color.Transparent),
                color = if (isSelected) Color.Blue else Color.Black,
                text = item.toString(),
                fontSize = 20.sp)
        }
    )
}

Preste atención a la forma en que predicateconecta el elemento del índice (una letra 'A' a 'Z') con la primera letra del apellido de la persona.

Implementación totalmente nueva de LazyColumn

A continuación puede ver la IndexedLazyColumnfunción sin su cuerpo.

@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
@Composable
fun <T> IndexedLazyColumn(
    indices: List<T>,
    itemsListState: LazyListState,
    modifier: Modifier = Modifier,
    predicate: (T) -> Int,
    lazyColumnContent: @Composable () -> Unit,
    indexedItemContent: @Composable (T, Boolean) -> Unit
)

Leyendo uno a uno los parámetros:

  • indices:Una lista simple de sus elementos de índice (por ejemplo, 1..10, A..Z, etc.).
  • itemsListState: Como dijimos, usas tu LazyColumny a través de este valor se conectará con los índices
  • modifier: Solo otro Modifiercomo el que estás usando con otros Composables.
  • predicate: Esta lambda aquí, es la conexión entre indicesy data. Puede escribir su propia lógica, pero debe tener cuidado de agregar siempre como parámetro un elemento de la indiceslista y devolver el índice de un elemento de data.
  • lazyColumnContent: Aquí está el Composable que te mostrará LazyColumnen la pantalla.
  • indexedIntemContent: Aquí está el Composable que mostrará el indiceselemento en la pantalla.

Aquí hay un ejemplo

@ExperimentalFoundationApi
@RequiresApi(Build.VERSION_CODES.N)
@Composable
fun ExampleIndexedLazyColumn(data: List<CustomListItem2>,
                       indices: List<Char> = ('A'..'Z').toList()) {
    val lazyListState = rememberLazyListState()

    IndexedLazyColumn(
        // The list of the indices
        indices = indices,
        // The state of the main LazyColumn, the one with the real data
        itemsListState = lazyListState,
        // The modifier is exported for the Column, the one with the indices
        modifier = Modifier
            .background(color = Color.Transparent)
            .height(300.dp),
        // The way to connect the index with a data item (here the index item matches the first letter of the surname)
        predicate = {
            data.indexOfFirst { item ->
                item.surname.startsWith(it.toString(), true)
            }
                    },
        // The list of the main data
        lazyColumnContent = {
            LazyColumn(state = lazyListState) {
                items(data) {
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(5.dp)
                            .clickable { },
                        elevation = 10.dp
                    ) {
                        Column {
                            Text(text = it.surname,
                                fontSize = 17.sp, fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic,
                                modifier = Modifier.padding(start = 10.dp))
                            Text(text = it.name,
                                fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
                                modifier = Modifier.padding(start = 15.dp))
                        }
                    }
                }
            }
        },
        // The item content for the indices list
        indexedItemContent = { item, isSelected ->
            Text(
                modifier = Modifier
                    .background(color = Color.Transparent),
                color = if (isSelected) Color.Blue else Color.Black,
                text = item.toString(),
                fontSize = 20.sp)
        }
    )
}

Preste atención a la forma en que predicateconecta el elemento del índice (una letra 'A' a 'Z') con la primera letra del apellido de la persona. Además, preste atención a cómo define a LazyListStatey páselo a la función ( itemsListStateparámetro) y a su LazyColumn( stateparámetro) también.

Producción

Los dos ejemplos anteriores representan los mismos datos y tienen la misma funcionalidad. ¡El siguiente video muestra lo que está haciendo todo este código!

https://youtu.be/-LvYbSgeMwU

solo necesito el codigo

Si desea que el código de la biblioteca se copie y pegue en su proyecto desde aquí . Si necesita ejecutar los ejemplos que mencioné anteriormente, obtenga el proyecto completo .

Problemas conocidos

Al usar el desplazamiento de la lista de índices, la lista de datos también se desplaza automáticamente (como vio en el video). Lo contrario, el desplazamiento de la lista de datos y el desplazamiento automático de la lista de índices, no funciona.

¿Le gustaría hacer de esta funcionalidad su próxima solicitud de extracción?

Fuente: https://betterprogramming.pub/lazycolumn-with-indexing-for-jetpack-compose-3456023d19d5

#jetpack 

Crear Una LazyColumn Con Indexación Para Jetpack Compose
伊藤  直子

伊藤 直子

1653499020

JetpackComposeのインデックスを使用してLazyColumnを作成する

にはRecyclerView「this」ファイルと「that」ファイル、およびそれらの多くの構成が必要になる場合がありますが、成熟したクラスであり、多くのライブラリが念頭に置いて構築されていることに異論はありませんRecyclerView

私は私が同じことを言うことができればいいのですがListViewListActivity私はしません!過去の罪について話すのはこれで十分です。JetpackComposeの時間なので、未来に固執する必要があります。

ダブルヘッダーLazyColumnとは別にRecyclerView、サイドにインデックスを付けたsを本当に見逃しました。ただし、これはJetpackの作成時間であるため、1つのことを実行できますLazyColumn。インデックスを使用して独自のカスタムを作成します。

このアプローチには、ほぼ同じ2つの実装があります。それらの主な違いは、IndexedLazyColumn実装には全体LazyColumnとそのLazyListStateasパラメーターがIndexedDataLazyColumn必要ですが、itemContentパラメーターfromからitemsのパラメーターが必要なことLazyColumnです。混乱していますか?心配はいりません。この記事の終わりまでに、前の文で良い視点が得られると思います。

 

シンプルなデータ実装

以下IndexedDataLazyColumnに、本体のない関数を示します。

@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
@Composable
fun <T> IndexedDataLazyColumn(
    indices: List<T>,
    data: List<T>,
    modifier: Modifier = Modifier,
    predicate: (T) -> Int,
    mainItemContent: @Composable LazyItemScope.(Int) -> Unit,
    indexedItemContent: @Composable (T, Boolean) -> Unit
)

パラメータを1つずつ読み取ります。

  • indices:インデックスアイテムの簡単なリスト(例:1..10、A..Zなど)。
  • data:データアイテム(映画、名前、自動車メーカー、ショッピングリストなど)を含むリスト。
  • modifierModifier他のコンポーザブルで使用しているものとまったく同じです。
  • predicate:ここでのこのラムダは、との間の接続indicesですdata。独自のロジックを自由に作成できますが、常にindicesリストからアイテムをパラメータとして追加し、からアイテムのインデックスを返すように注意する必要がありますdata
  • mainItemContentdata:画面にアイテムを表示するコンポーザブルです。
  • indexedItemContentindices:画面にアイテムを表示するコンポーザブルです。

これが例です

@ExperimentalFoundationApi
@RequiresApi(Build.VERSION_CODES.N)
@Composable
fun ExampleIndexedDataLazyColumn(data: List<CustomListItem2>,
                       indices: List<Char> = ('A'..'Z').toList()) {

    IndexedDataLazyColumn(
        // The list of the indices
        indices = indices,
        // The list of the actual data
        data = data,
        // The modifier is exported for the Column, the one with the indices
        modifier = Modifier
            .background(color = Color.Transparent)
            .height(300.dp),
        // The way to connect the index with a data item (here the index item matches the first letter of the surname)
        predicate = {
            data.indexOfFirst { item ->
                item.surname.startsWith(it.toString(), true) }
        },
        // The list of the main data
        mainItemContent = {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(5.dp)
                    .clickable { },
                elevation = 10.dp
            ) {
                Column {
                    Text(text = data[it].surname,
                        fontSize = 17.sp, fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic,
                        modifier = Modifier.padding(start = 10.dp))
                    Text(text = data[it].name,
                        fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
                        modifier = Modifier.padding(start = 15.dp))
                }
            }
        },
        // The item content for the indices list
        indexedItemContent = { item, isSelected ->
            Text(
                modifier = Modifier
                    .background(color = Color.Transparent),
                color = if (isSelected) Color.Blue else Color.Black,
                text = item.toString(),
                fontSize = 20.sp)
        }
    )
}

predicateインデックスアイテム(文字「A」から「Z」)を人の名前の最初の文字に接続する方法に注意してください。

まったく新しいLazyColumnの実装

以下IndexedLazyColumnに、本体のない関数を示します。

@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
@Composable
fun <T> IndexedLazyColumn(
    indices: List<T>,
    itemsListState: LazyListState,
    modifier: Modifier = Modifier,
    predicate: (T) -> Int,
    lazyColumnContent: @Composable () -> Unit,
    indexedItemContent: @Composable (T, Boolean) -> Unit
)

パラメータを1つずつ読み取ります。

  • indices:インデックスアイテムの簡単なリスト(例:1..10、A..Zなど)。
  • itemsListState:私たちが言ったように、あなたはあなたを使用LazyColumnし、この値を通してそれはインデックスに接続されます
  • modifierModifier他のコンポーザブルで使用しているものとまったく同じです。
  • predicate:ここでのこのラムダは、との間の接続indicesですdata。独自のロジックを自由に作成できますが、常にindicesリストからアイテムをパラメータとして追加し、からアイテムのインデックスを返すように注意する必要がありますdata
  • lazyColumnContent: LazyColumnこれが画面に表示されるコンポーザブルです。
  • indexedIntemContentindices:画面にアイテムを表示するコンポーザブルです。

これが例です

@ExperimentalFoundationApi
@RequiresApi(Build.VERSION_CODES.N)
@Composable
fun ExampleIndexedLazyColumn(data: List<CustomListItem2>,
                       indices: List<Char> = ('A'..'Z').toList()) {
    val lazyListState = rememberLazyListState()

    IndexedLazyColumn(
        // The list of the indices
        indices = indices,
        // The state of the main LazyColumn, the one with the real data
        itemsListState = lazyListState,
        // The modifier is exported for the Column, the one with the indices
        modifier = Modifier
            .background(color = Color.Transparent)
            .height(300.dp),
        // The way to connect the index with a data item (here the index item matches the first letter of the surname)
        predicate = {
            data.indexOfFirst { item ->
                item.surname.startsWith(it.toString(), true)
            }
                    },
        // The list of the main data
        lazyColumnContent = {
            LazyColumn(state = lazyListState) {
                items(data) {
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(5.dp)
                            .clickable { },
                        elevation = 10.dp
                    ) {
                        Column {
                            Text(text = it.surname,
                                fontSize = 17.sp, fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic,
                                modifier = Modifier.padding(start = 10.dp))
                            Text(text = it.name,
                                fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
                                modifier = Modifier.padding(start = 15.dp))
                        }
                    }
                }
            }
        },
        // The item content for the indices list
        indexedItemContent = { item, isSelected ->
            Text(
                modifier = Modifier
                    .background(color = Color.Transparent),
                color = if (isSelected) Color.Blue else Color.Black,
                text = item.toString(),
                fontSize = 20.sp)
        }
    )
}

predicateインデックスアイテム(文字「A」から「Z」)を人の名前の最初の文字に接続する方法に注意してください。LazyListStateまた、aを定義し、それを関数(itemsListStateパラメーター)と自分のLazyColumn(パラメーター)に渡す方法にも注意してくださいstate

出力

前の例は両方とも同じデータをレンダリングしており、同じ機能を持っています。次のビデオは、このコードのすべてが何をしているのかを示しています!

https://youtu.be/-LvYbSgeMwU

コードが必要です

ライブラリコードをコピーしてプロジェクトに貼り付ける場合は、ここから。上記の例を実行する必要がある場合は、プロジェクト全体を取得してください。

既知の問題点

インデックスリストのスクロールを使用すると、データリストも自動スクロールされます(ビデオで見たように)。反対に、データリストをスクロールしてインデックスリストを自動スクロールしても機能しません。

この機能を次のプルリクエストにしますか?

ソース:https ://betterprogramming.pub/lazycolumn-with-indexing-for-jetpack-compose-3456023d19d5

#jetpack 

JetpackComposeのインデックスを使用してLazyColumnを作成する
Joseph  Norton

Joseph Norton

1653275357

Android Studio Tutorial | Build a Calculator with Jetpack Compose

How to Build a Calculator with Jetpack Compose - Android Studio Tutorial

In this video, we'll build a simple calculator using Jetpack Compose :)

Get the source code for this video here:
https://github.com/philipplackner/CalculatorPrep 


Jetpack Compose: Calculator UI

Note: I’m using Compose version 0.1.0-dev09 at the time of writing this.

Ok — so the thing that’s complex about the Calculator UI is that it has multiple draggable panels and overlays.

What it looks like:

The Code

You can get the source code here

Starting with the top most level composable

 setContent {
    MaterialTheme(colors = lightThemeColors) {
        WithConstraints { constraints, _ ->
            val boxHeight = with(DensityAmbient.current) { constraints.maxHeight.toDp() }
            val boxWidth = with(DensityAmbient.current) { constraints.maxWidth.toDp() }
            Content(
                constraints = constraints,
                boxHeight = boxHeight,
                boxWidth = boxWidth
            )
        }
    }
}

The hierarchy goes like: MaterialTheme → WithConstraints → Content

MaterialTheme

It’s a high level component that sets the pre-defined colors and styling according to the Material design principles.

WithConstraints

For getting the available screen height and width — these constraints are later used for setting and defining anchors for draggable panels.

The Components

We got two draggable panels (the TopView and SideView). I’ll be focusing on these (as the rest of the app is pretty straight forward).

Side View

Let’s start with the side view. That’s the blue panel that slides from right to left. It’s either in collapsed state or expanded.

This is what it looks like when expanded:

This panel has drag and fling property. In order to add this slide behaviour — we have to use three things.

  1. AnimatedFloat
  2. FlingConfig
  3. Draggable Modifier

AnimatedFloat

For animating values between bounds. Inherits from BaseAnimatedValue.

FlingConfig

For adding fling — when drag ends, it figures out whether to fling to start or end given velocity. AnchorsFlingConfig is used for flinging between a predefined set of values (anchors).

Draggable Modifier

A modifier that adds drag to a single view. Used for when we want to drag a child component. It has these params:

dragDirection direction in which drag should be happening
onDragDeltaConsumptionRequested callback to be invoked when drag occurs. Users must update their state in this lambda and return amount of delta consumed
onDragStarted callback that will be invoked when drag has been started after touch slop has been passed, with starting position provided
onDragStopped callback that will be invoked when drag stops, with velocity provided
enabled whether or not drag is enabled
startDragImmediately when set to true, draggable will start dragging immediately and prevent other gesture detectors from reacting to "down" events (in order to block composed press-based gestures). This is intended to allow end users to "catch" an animating widget by pressing on it. It's useful to set it when value you're dragging is settling / animating.

I also used a helper model class for passing drag/fling information around.

class Drag(
    val position: AnimatedFloat,
    val flingConfig: FlingConfig
)

Side View Drag

// Side drag
val sideMin = 90.dp
val sideMax = boxWidth - 30.dp
val (sideMinPx, sideMaxPx) = with(DensityAmbient.current) {
    sideMin.toPx().value to sideMax.toPx().value
}
val sideFlingConfig = AnchorsFlingConfig(listOf(sideMinPx, sideMaxPx))
val sidePosition = animatedFloat(sideMaxPx)
sidePosition.setBounds(sideMinPx, sideMaxPx)

val sideDrag = Drag(
    position = sidePosition,
    flingConfig = sideFlingConfig
)

Specify min and max values (calculated pixel locations on screen) as the animated values and anchors for the fling. And then use this information in SideView composable like this:

@Composable()
fun SideView(
    boxHeight: Dp,
    drag: Drag
) {
    val position = drag.position
    val flingConfig = drag.flingConfig
    val yOffset = with(DensityAmbient.current) { position.value.toDp() }
    val toggleAsset = state { R.drawable.ic_keyboard_arrow_left_24 }
    Box(
        Modifier.offset(x = yOffset, y = 0.dp)
            .fillMaxWidth()
            .draggable(
                startDragImmediately = position.isRunning,
                dragDirection = DragDirection.Horizontal,
                onDragStopped = { position.fling(flingConfig, it) }
            ) { delta ->
                position.snapTo(position.value + delta)
                delta
            }
            .preferredHeight(boxHeight),
        backgroundColor = AppState.theme.primary,
        gravity = ContentGravity.CenterStart
    ) {
       ...
    }
}

In the composable — the drag position is retrieved, and horizontal offset is calculated. The flingConfig is used in onDragStopped callback.

And that’s it — the drag and fling works!

Top View

Similar to the side view — it’s also a draggable component, but from top to bottom.

// Top drag
val topStart = -(constraints.maxHeight.value / 1.4f)
val topMax = 0.dp
val topMin = -(boxHeight / 1.4f)
val (topMinPx, topMaxPx) = with(DensityAmbient.current) {
    topMin.toPx().value to topMax.toPx().value
}
val topFlingConfig = AnchorsFlingConfig(listOf(topMinPx, topMaxPx))
val topPosition = animatedFloat(topStart) // for dragging state
topPosition.setBounds(topMinPx, topMaxPx)

val topDrag = Drag(
    position = topPosition,
    flingConfig = topFlingConfig
)

Just like before — we calculate the min and max values as animated float values and anchors for the fling. And then use this information in TopView composable like this:

@Composable
fun TopView(
    boxHeight: Dp,
    drag: Drag
) {
    val position = drag.position
    val flingConfig = drag.flingConfig
    val yOffset = with(DensityAmbient.current) { position.value.toDp() }
    val scrollerPosition = ScrollerPosition()
    // scroll the history list to bottom when dragging the top panel
    // 90dp history item height is an approximation
    scrollerPosition.smoothScrollBy(operationsHistory.size * 90.dp.value)

    Card(
        Modifier.offset(y = yOffset, x = 0.dp).fillMaxWidth()
            .draggable(
                startDragImmediately = position.isRunning,
                dragDirection = DragDirection.Vertical,
                onDragStopped = { position.fling(flingConfig, it) }
            ) { delta ->
                position.snapTo(position.value + delta)
                // consume all delta no matter the bounds to avoid nested dragging (as example)
                delta
            }
            .preferredHeight(boxHeight),
        elevation = 4.dp,
        shape = MaterialTheme.shapes.large,
        color = Color.White
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Bottom,
            horizontalGravity = ContentGravity.CenterHorizontally
        ) {
            HistoryTopBar()
            HistoryList(scrollerPosition)
            Spacer(modifier = Modifier.preferredHeight(2.dp))
            CollapsableContent(boxHeight, position)
            RoundedDash()
        }
    }
}

Used Card for elevation, and a custom scroller position so that the scroller stays scrolled to bottom when dragging.

Inside the Card composable, we simply have a column with a few components

As the drag position changes — I hide and shrink MainContent. That gives us the cool sliding down effect for HistoryList.

Overlays

I created a box component with background color, passing in the alpha value.

@Composable
fun DimOverlay(alpha: Int) {
    Box(modifier = Modifier.fillMaxSize(), backgroundColor = Color(50, 50, 50, alpha))
}

This overlay is placed on top of Numbers Panel — and the alpha is calculated based on drag. I used the Stack composable, so that it’s placed over the views.

@Composable()
fun NumbersPanel(alpha: Int) {
    Stack {
        Row(modifier = Modifier.fillMaxSize()) {
            numberColumns.forEach { numberColumn ->
                Column(modifier = Modifier.weight(1f)) {
                    numberColumn.forEach { text ->
                        MainContentButton(text)
                    }
                }
            }
            Divider(
                modifier = Modifier.preferredWidth(1.dp).fillMaxHeight(),
                color = Color(0xFFd3d3d3)
            )
            Column(modifier = Modifier.weight(1.3f)) {
                operationsColumn.forEach { operation ->
                    OperationItem(text = operation)
                }
            }
            Spacer(modifier = Modifier.preferredWidth(30.dp))
        }
        DimOverlay(alpha = alpha)
    }
}

This is how the numbers dim out as I slide out the Side View. Similar concept is used for the Top View overlay.

Limitations

There were some limitations as well (as Compose isn’t production-ready).

AdapterList: There wasn’t much control over scrolling state so I had to opt for VerticalScroller instead for showing history list.

TextField: No RTL mode & couldn’t disable soft input

#android #jetpack #jetpackcompose

Android Studio Tutorial | Build a Calculator with Jetpack Compose
Corey Brooks

Corey Brooks

1652322199

Building a Wellness App with State in Jetpack Compose

Using state in Jetpack Compose

Discover the core concepts of using state in Jetpack Compose by building a wellness app. Learn how the app's state determines what is displayed in the UI, how Compose keeps the UI updated when state changes, how to optimize the structure of your composable functions, and work with ViewModels in a Compose app.

#android #jetpack 

Building a Wellness App with State in Jetpack Compose
Debbie Clay

Debbie Clay

1652238197

How to Build a Beautiful Android App using Kotlin and Android Studio

Jetpack Compose for designers - SwiftUI for Android?

Learn the fundamentals of Jetpack Compose to build a beautiful Android application using Kotlin and Android Studio

#jetpack #swiftui #android

How to Build a Beautiful Android App using Kotlin and Android Studio
Coding  Life

Coding Life

1652237742

Jetpack Compose | build a beautiful Android application using Kotlin

Learn the fundamentals of Jetpack Compose to build a beautiful Android application using Kotlin and Android Studio.

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.

Subscribe: https://www.youtube.com/c/DesignCodeTeam/featured 

#jetpack #kotlin #androidstudio 

Jetpack Compose |  build a beautiful Android application using Kotlin

Almacenamiento Genérico De Datos Persistentes En Android

De una forma u otra, cualquier aplicación moderna de Android almacena algunos datos de usuario o de configuración localmente en el dispositivo. En el pasado, los desarrolladores confiaban en la API SharedPreferences para almacenar datos simples en pares clave-valor.

Donde la API SharedPreferences no brilla es en su API síncrona para operaciones de lectura y escritura. Dado que Android frunce el ceño al realizar trabajos que no son de interfaz de usuario en el hilo principal, esto no es seguro de usar.

En este artículo, aprenderá a usar la API de DataStore con almacenamiento persistente genérico. Este enfoque nos permitirá crear una clase de almacenamiento donde podemos especificar cualquier tipo de datos que deseemos guardar como un par clave-valor en el dispositivo.

Ventajas de usar Jetpack DataStore

  • DataStore es totalmente asincrónico, utilizando corrutinas de Kotlin
  • Las operaciones de lectura y escritura se realizan en segundo plano, sin temor a bloquear la interfaz de usuario
  • Con las corrutinas, existen mecanismos para la señalización de errores cuando se usa DataStore.

Configuración de una aplicación de Android de muestra

En esta demostración, crearemos una aplicación de muestra para obtener las configuraciones de la aplicación desde una fuente en memoria y guardarlas en el dispositivo usando DataStore.

Hay algunos requisitos previos antes de que podamos comenzar:

  • Conocimientos básicos de desarrollo móvil Android y Kotlin
  • Android Studio instalado en tu PC

Comencemos por crear un proyecto de Android Studio vacío.

Copie y pegue las siguientes dependencias en su build.gradlearchivo de nivel de aplicación.

implementación "androidx.datastore:datastore-preferences:1.0.0" implementación "io.insert-koin:koin-android:3.1.4" implementación 'com.google.code.gson:gson:2.8.7'

Junto a la dependencia de DataStore están las dependencias extra koiny gson, que son para inyección de dependencia y serialización/deserialización, respectivamente.

Después de insertar estas dependencias, Android Studio le pedirá que sincronice el proyecto. Esto suele tardar unos segundos.

Creación de una interfaz de almacenamiento de Kotlin

Cree un archivo de interfaz de Kotlin, así.

interface Storage<T> {

    fun insert(data: T): Flow<Int>

    fun insert(data: List<T>): Flow<Int>

    fun get(where: (T) -> Boolean): Flow<T>

    fun getAll(): Flow<List<T>>

    fun clearAll(): Flow<Int
}

Usamos una interfaz de almacenamiento para definir las acciones para el almacenamiento persistente de datos. En otras palabras, es un contrato que cumplirá el almacenamiento persistente. Cualquier tipo de datos que pretendamos asociar con la interfaz debería poder realizar las cuatro operaciones en la interfaz que creamos.

Creación de una implementación concreta de la interfaz de almacenamiento

PersistentStoragees la implementación concreta de la Storageinterfaz que definimos en el paso anterior.

class PersistentStorage<T> constructor(
    private val gson: Gson,
    private val type: Type,
    private val dataStore: DataStore<Preferences>,
    private val preferenceKey: Preferences.Key<String>
) : Storage<T>

Ya observará que estamos aprovechando los genéricos en Storagey PersistentStorage. Esto se hace para lograr la seguridad del tipo. Si su código se basa en almacenamiento persistente genérico para almacenar datos, solo se asociará un tipo de datos con una instancia particular de Storage.

También se requieren varias dependencias de objetos:

  1. gson: Como se mencionó anteriormente, esto se usará para la serialización/deserialización
  2. type: Nuestra implementación brinda al usuario la flexibilidad de guardar más de un dato del mismo tipo, y un gran poder conlleva una gran responsabilidad. Escribir y leer una lista con GSON dará como resultado datos dañados o perdidos porque Java aún no proporciona una forma de representar tipos genéricos, y GSON no puede reconocer qué tipo usar para su conversión en tiempo de ejecución, por lo que usamos un token de tipo para efectivamente convertir nuestros objetos a una cadena JSON y viceversa sin ninguna complicación
  3. Clave de preferencia: este es un objeto específico de Android Jetpack DataStore; es básicamente una clave para guardar y recuperar datos deDataStore
  4. DataStore: Esto proporcionará API para escribir y leer desde las preferencias

Implementando la getAlloperación

...
fun getAll(): Flow<List> {
    return dataStore.data.map { preferences ->
        val jsonString = preferences[preferenceKey] ?: EMPTY_JSON_STRING
        val elements = gson.fromJson<List>(jsonString, typeToken)
        elements
    }
}
...

DataStore.datadevuelve un flujo de preferencias con Flow<Preferences>, que se puede transformar en un Flow<List<T>>utilizando el mapoperador. Dentro del bloque del mapa, primero intentamos recuperar la cadena JSON con la clave de preferencia.

En el caso de que el valor sea null, asignamos EMPTY_JSON_STRINGa jsonString. EMPTY_JSON_STRINGes en realidad una variable constante, definida así:

private const val EMPTY_JSON_STRING = "[]"

GSON lo reconocerá convenientemente como una cadena JSON válida, que representa una lista vacía del tipo especificado. Este enfoque es más lógico, en lugar de generar alguna excepción que podría causar un bloqueo en la aplicación. Estoy seguro de que no queremos que eso suceda en nuestras aplicaciones 🙂

Implementando la insertoperación

fun insert(data: List<T>): Flow<Int> {
    return flow {
        val cachedDataClone = getAll().first().toMutableList()
        cachedDataClone.addAll(data)
        dataStore.edit {
            val jsonString = gson.toJson(cachedDataClone, type)
            it[preferenceKey] = jsonString
            emit(OPERATION_SUCCESS)
        }
    }
}

Para escribir datos en DataStore, llamamos a editar en Datastore. Dentro del bloque de transformación, editamos el MutablePreferences, como se muestra en el bloque de código anterior.

Para evitar sobrescribir los datos antiguos con los nuevos, creamos una lista que contiene datos antiguos y nuevos antes de modificar MutablePreferencescon la lista recién creada.

nb, opté por usar la sobrecarga de métodos para insertar una sola o una lista de datos sobre un parámetro vararg porque los varargs en Kotlin requieren memoria adicional al copiar la lista de datos en una matriz.

Implementando la getoperación

fun get(where: (T) -> Boolean): Flow {
    return getAll().map { cachedData ->
        cachedData.first(where)
    }
}

En esta operación, queremos obtener un único dato del almacén que coincida con el predicado where. Este predicado se implementará en el lado del cliente.

Implementando la clearAlloperación

fun clearAll(): Flow<Int> {
    return flow {
        dataStore.edit {
            it.remove(preferenceKey)
            emit(OPERATION_SUCCESS)
        }
    }
}

Como su nombre lo indica, queremos borrar los datos asociados con la preferenceclave. emit(OPERATION_SUCCESS)es nuestra forma de avisar al cliente de una operación exitosa.

En este punto, hemos hecho justicia a las API de almacenamiento genéricas. A continuación, configuraremos la clase modelo y una fuente de datos en memoria.

Crear la modelclase y la fuente de datos en memoria

Cree una Configclase de datos, así:

data class Config(val type: String, val enable: Boolean)

Para simplificar las cosas, esta clase de datos solo captura un tipo de configuración y su valor de alternancia correspondiente. Dependiendo de su caso de uso, su clase de configuración puede describir muchas más acciones.

class DataSource {

    private val _configs = listOf(
        Config("in_app_billing", true),
        Config("log_in_required", false),
    )

    fun getConfigs(): Flow<List<Config>> {
        return flow {
            delay(500) // mock network delay
            emit(_configs)
        }
    }
}

A falta de un servidor real al que conectarse, tenemos nuestras configuraciones almacenadas en la memoria y recuperadas cuando es necesario. También hemos incluido un retraso para simular una llamada de red real.

Cómo inyectar dependencias con Koin

Si bien este artículo se centra en la creación de una aplicación Android de demostración minimalista, está bien adoptar algunas prácticas modernas. Implementaremos el código para obtener configuraciones a través de a ViewModely proporcionaremos dependencias a los objetos donde sea necesario usando koin .

¿Qué es Koin?

Koin es un poderoso marco de inyección de dependencia de Kotlin. Tiene API simples y es relativamente fácil de configurar.

crear una ViewModelclase

class MainViewModel(
    private val dataSource: DataSource,
    private val configStorage: Storage<Config>
) : ViewModel() {

    init {
        loadConfigs()
    }

    private fun loadConfigs() = viewModelScope.launch {
        dataSource
            .getConfigs()
            .flatMapConcat(configStorage::insert)
            .collect()
    }
}

Aquí, obtenemos las configuraciones de una fuente de datos y las guardamos en nuestras preferencias de DataStore.
La intención es poder recuperar esas configuraciones localmente sin tener que hacer llamadas de red adicionales al servidor. El lugar más obvio para iniciar esta solicitud sería en el lanzamiento de la aplicación.

Defina sus módulos koin así:

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "com.enyason.androiddatastoreexample.shared.preferences")

val dataAccessModule = module {

    single<Storage<Config>> {
        PersistentStorage(
            gson = get(),
            type = object : TypeToken<List<Config>>() {}.type,
            preferenceKey = stringPreferencesKey("config"),
            dataStore = androidContext().dataStore
        )
    }

    single { Gson() }

    viewModel {
        MainViewModel(
            dataSource = DataSource(),
            configStorage = get()
        )
    }

}

Ahora hemos delegado el trabajo pesado a Koin. Ya no tenemos que preocuparnos de cómo se crean los objetos: Koin maneja todo eso por nosotros.

La singledefinición le dice a Koin que cree solo una instancia del tipo especificado a lo largo del ciclo de vida de la aplicación. La viewModeldefinición le dice a Koin que cree solo un tipo de objeto que amplíe la ViewModelclase de Android.

Inicializando Koin para preparar dependencias

Necesitamos inicializar Koin para preparar nuestras dependencias antes de que nuestra aplicación las solicite. Crea una Applicationclase, así:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@App)
            modules(dataAccessModule)
        }
    }
}

Finalmente hemos conectado todas las piezas juntas, y nuestro proyecto ahora debería funcionar como se esperaba. Consulte este repositorio de GitHub para ver la configuración completa del proyecto.

Beneficios del almacenamiento persistente genérico con Android DataStore

  • Las API de DataStore funcionan con corrutinas de Kotlin bajo el capó, lo que hace que el subproceso de almacenamiento persistente genérico sea seguro, a diferencia de la API SharedPreferences
  • La lógica de lectura y escritura se escribe solo una vez para cualquier tipo de objeto
  • Garantía de seguridad de tipo: Storage<Config>está seguro de recuperar solo los datos de Configtipo
  • PreferenceHelperLas clases, que están destinadas a administrar las preferencias de la aplicación, generalmente dan como resultado clases monolíticas, lo cual es una mala práctica de ingeniería de software. Con el enfoque genérico discutido en este artículo, puede lograr más con menos código
  • Podemos realizar pruebas unitarias de manera efectivaPersistentStorage<T>

Conclusión

La implementación de almacenamiento persistente genérico es una forma elegante de administrar datos con Android DataStore. Las ganancias, como he comentado anteriormente, superan el enfoque tradicional en Android con SharedPreference. Espero que les haya gustado este tutorial 😊

Fuente: https://blog.logrocket.com/generic-persistent-data-storage-android-using-jetpack-datastore/

#android #jetpack 

Almacenamiento Genérico De Datos Persistentes En Android