1655199027
Serverless Jetpack 🚀
A faster JavaScript packager for Serverless applications.
serverless package|deploy
lerna
, yarn workspace
) supportThe 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.
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! 🚀
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"
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).servicePath
.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]).[servicePath]
with the new array, so if you want to additionally keep the servicePath
in the roots array, set as: [".", ADDITION_01, ADDITION_02, ...]
.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.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.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.include
s. 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
).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"
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.
Let's start by looking at how Serverless packages (more or less):
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.**
(all files) and the include
pattern, following symlinks, and create a list of files (no directories). This is again disk I/O.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:
devDependencies
.**
(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.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:
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? 😉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):
serverless-plugin-typescript
: See #74serverless-webpack
: See, e.g. packageModules.js
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
...
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:
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
.base
and roots
options to the root project servicePath
for dependency searching and not relatively to layer path
s.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.
**
(everything).jetpack.preInclude
patterns.include
production node_modules
.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).
jetpack.preInclude
patterns.include
production node_modules
.package.exclude
patterns.exclude
development node_modules
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/**"
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.json
s: 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.
ℹ️ 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:
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.
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.
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.!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.node_modules
), specify the relative path (from servicePath
/ CWD) to it like "./src/server/router.js": [/* array of patterns */]
../src/server.js
, ../lower/src/server.js
). Basically, like the Node.js require()
rules go for a local path file vs. a package dependency.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 */]
.[]
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.include
s 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
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:
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.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|excludes
s: The normal package include|exclude
s 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 require
s that dynamically inject a variable etc. like require(myVariable)
.
WARNING
log output for the list of files and read our section below on handling dynamic imports.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:
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
.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.include
are 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.
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 scenariosType
: 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:
Scenario | Type | Zips | Files | Size | vs Base |
---|---|---|---|---|---|
simple | jetpack | 1 | 200 | 529417 | -42.78 % |
simple | baseline | 1 | 433 | 925260 | |
complex | jetpack | 2 | 1588 | 3835544 | -18.20 % |
complex | baseline | 2 | 2120 | 4688648 |
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.)
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:
darwin 18.7.0 x64
v12.14.1
Results:
Scenario | Pkg | Type | Mode | Time | vs Base |
---|---|---|---|---|---|
simple | yarn | jetpack | trace | 4878 | -74.25 % |
simple | yarn | jetpack | deps | 3861 | -79.62 % |
simple | yarn | baseline | 18941 | ||
simple | npm | jetpack | trace | 7290 | -68.34 % |
simple | npm | jetpack | deps | 4017 | -82.55 % |
simple | npm | baseline | 23023 | ||
complex | yarn | jetpack | trace | 10475 | -70.93 % |
complex | yarn | jetpack | deps | 8821 | -75.52 % |
complex | yarn | baseline | 36032 | ||
complex | npm | jetpack | trace | 15644 | -59.13 % |
complex | npm | jetpack | deps | 9896 | -74.15 % |
complex | npm | baseline | 38282 |
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
1654502018
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
1654265520
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.
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:
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.
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.
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 .
Dentro build.gradle
del 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 kapt
complementos de empuñadura en la parte superior del build.gradle
archivo.
plugins {
// Other plugins...
id 'dagger.hilt.android.plugin'
id 'kotlin-kapt'
}
Agregar classpath al build.gradle
archivo (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.gradle
archivo. Si está utilizando Android Studio Canary, debe agregar esta línea a su settings.gradle
archivo. Luego haga clic en el botón sincronizar ahora.
maven { url 'https://jitpack.io' }
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)
}
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)
}
}
Cree WalletConnectViewModel
e 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 connectWallet
la 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
1654265009
ブロックチェーンテクノロジーの成長は非常に急速であるため、暗号通貨について聞いたことがない、またはその仕事について知らない人でも、この分野に投資して探索しようとしています。The Independentが実施した調査によると、調査対象の英国人の11%が、暗号通貨やブロックチェーンテクノロジーに慣れれば、それに興味があると答えています。暗号通貨に関する話題が世界中に広がり続け、より多くのスタートアップがブロックチェーンテクノロジーを使用する新しい方法を毎日開発しているので、より多くの人々がこの分野への投資に興味を持っているのも不思議ではありません。
この短い記事では、JetpackComposeとWalletConnectを使用して暗号ウォレットをAndroidアプリに接続する方法を学習します。
私たちが暗号通貨について話すとき、CryptoWalletと呼ばれる最も重要なものがあります。CrypoWalletsは、デジタル資産を保存し、トランザクションを検証するように設計されています。ウォレットは、トランザクションの署名に使用される秘密鍵を使用して秘密情報を保持します。暗号通貨ウォレットは、コインやトークンを安全に保管および管理する方法ですが、市場にはいくつかの異なるタイプのウォレットがあります。
WalletConnectは、WalletとDapps(Web3 Apps)の間で安全に通信するためのオープンプロトコルです。プロトコルは、ペイロードを中継するためにBridgeサーバーを使用して、2つのアプリやデバイス間のリモート接続を確立します。これらのペイロードは、2つのピア間の共有キーを介して対称的に暗号化されます。
接続は、QRコードまたは標準のWalletConnect URIを使用したディープリンクを表示する1つのピアによって開始され、カウンターパーティがこの接続要求を承認したときに確立されます。また、オプションのプッシュサーバーが含まれており、ネイティブアプリケーションが確立された接続の着信ペイロードをユーザーに通知できるようにします。
Jetpack Composeは、GoogleDevelopersによって開発されたネイティブUIを作成するためのAndroidモダンツールです。Jetpack Composeは、iOSアプリ開発で使用されるSwiftUIに例えられます。この例えは、より簡単な設計、より少ないコード、およびより多くのビジネスロジックに基づいています。
Jetpack Composeは、AndroidアプリでのUI開発を簡素化し、アプリ開発を高速化します。Jetpack Composeのモットーは、コードの削減、強力なツール、直感的なKotlin API、および迅速なアプリケーション開発です。
このセクションでは、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)
}
アプリをウォレットに接続するには、ソケットサーバーが必要です。このサーバーを生成する方法のコードは次のとおりです。
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)
}
}
WalletConnectViewModel
Moshi、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
1654152684
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 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.
@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)
)
}
}
@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)
}
}
}
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.
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
1653896799
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
1653563055
Este artículo supone que el lector tiene conocimientos básicos sobre cómo funciona Jetpack Compose, si no está familiarizado con las @Composable
funciones 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.
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 backgroundColor
antes del clip
modificador. 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 clip
modificador 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
)
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 content
lambda está envuelta dentro de un ámbito, por ejemplo, en las columnas que tenemos ColumnScope
:
ColumnScope
es 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
, align
y 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 align
modificador a nuestros Text
Composables, 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 align
modificadores a nuestros Text
componibles, tenemos control directo de su posición horizontal dentro de la Columna.
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 makeColouredCircle
que 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.then
declaración. La then
funció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
yaspectRatio
debe usarse directamente en nuestroColumn
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")
}
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 clickable
modificador, esto permite que el componente reciba eventos de clic o clic de accesibilidad.
Tenga en cuenta que simplemente estamos agregando el onClick
pará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 clickable
modificador 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!
https://developer.android.com/jetpack/compose/modifiers
https://developer.android.com/jetpack/compose/modifiers-list
1653562860
この記事は、読者が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
)
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
andaspectRatio
should be used directly in ourColumn
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
1653499260
Es RecyclerView
posible 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 RecyclerView
en cuenta.
¡Ojalá pudiera decir lo mismo de ListView
y ListActivity
pero 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 RecyclerView
s 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 LazyColumn
con indexación.
El enfoque tiene dos implementaciones casi idénticas. La principal diferencia entre ellos es que la IndexedLazyColumn
implementación necesita un todo LazyColumn
y sus LazyListState
parámetros, pero IndexedDataLazyColumn
necesita el itemContent
parámetro, desde el items
pará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!
A continuación puede ver la IndexedDataLazyColumn
funció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 Modifier
como el que estás usando con otros Composables.predicate
: Esta lambda aquí, es la conexión entre indices
y data
. Puede escribir su propia lógica, pero debe tener cuidado de agregar siempre como parámetro un elemento de la indices
lista y devolver el índice de un elemento de data
.mainItemContent
: Aquí está el Composable que mostrará el data
elemento en la pantalla.indexedItemContent
: Aquí está el Composable que mostrará el indices
elemento 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 predicate
conecta el elemento del índice (una letra 'A' a 'Z') con la primera letra del apellido de la persona.
A continuación puede ver la IndexedLazyColumn
funció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 LazyColumn
y a través de este valor se conectará con los índicesmodifier
: Solo otro Modifier
como el que estás usando con otros Composables.predicate
: Esta lambda aquí, es la conexión entre indices
y data
. Puede escribir su propia lógica, pero debe tener cuidado de agregar siempre como parámetro un elemento de la indices
lista y devolver el índice de un elemento de data
.lazyColumnContent:
Aquí está el Composable que te mostrará LazyColumn
en la pantalla.indexedIntemContent
: Aquí está el Composable que mostrará el indices
elemento 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 predicate
conecta 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 LazyListState
y páselo a la función ( itemsListState
parámetro) y a su LazyColumn
( state
pará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!
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
1653499020
にはRecyclerView
「this」ファイルと「that」ファイル、およびそれらの多くの構成が必要になる場合がありますが、成熟したクラスであり、多くのライブラリが念頭に置いて構築されていることに異論はありませんRecyclerView
。
私は私が同じことを言うことができればいいのですがListView
、ListActivity
私はしません!過去の罪について話すのはこれで十分です。JetpackComposeの時間なので、未来に固執する必要があります。
ダブルヘッダーLazyColumnとは別にRecyclerView
、サイドにインデックスを付けたsを本当に見逃しました。ただし、これはJetpackの作成時間であるため、1つのことを実行できますLazyColumn
。インデックスを使用して独自のカスタムを作成します。
このアプローチには、ほぼ同じ2つの実装があります。それらの主な違いは、IndexedLazyColumn
実装には全体LazyColumn
とそのLazyListState
asパラメーターが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
:データアイテム(映画、名前、自動車メーカー、ショッピングリストなど)を含むリスト。modifier
:Modifier
他のコンポーザブルで使用しているものとまったく同じです。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」)を人の名前の最初の文字に接続する方法に注意してください。
以下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
し、この値を通してそれはインデックスに接続されますmodifier
:Modifier
他のコンポーザブルで使用しているものとまったく同じです。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 ://betterprogramming.pub/lazycolumn-with-indexing-for-jetpack-compose-3456023d19d5
1653275357
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
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
It’s a high level component that sets the pre-defined colors and styling according to the Material design principles.
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.
For animating values between bounds. Inherits from BaseAnimatedValue.
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).
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 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
1652322199
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
1652238197
Learn the fundamentals of Jetpack Compose to build a beautiful Android application using Kotlin and Android Studio
#jetpack #swiftui #android
1652237742
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
1650475980
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.
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:
Comencemos por crear un proyecto de Android Studio vacío.
Copie y pegue las siguientes dependencias en su build.gradle
archivo 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 koin
y 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.
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.
PersistentStorage
es la implementación concreta de la Storage
interfaz 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 Storage
y 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:
gson
: Como se mencionó anteriormente, esto se usará para la serialización/deserializacióntype
: 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ónDataStore
DataStore
: Esto proporcionará API para escribir y leer desde las preferenciasgetAll
operació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.data
devuelve un flujo de preferencias con Flow<Preferences>
, que se puede transformar en un Flow<List<T>>
utilizando el map
operador. 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_STRING
a jsonString
. EMPTY_JSON_STRING
es 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 🙂
insert
operaciónfun 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 MutablePreferences
con 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.
get
operaciónfun 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.
clearAll
operaciónfun 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 preference
clave. 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.
model
clase y la fuente de datos en memoriaCree una Config
clase 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.
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 ViewModel
y proporcionaremos dependencias a los objetos donde sea necesario usando koin .
Koin es un poderoso marco de inyección de dependencia de Kotlin. Tiene API simples y es relativamente fácil de configurar.
ViewModel
claseclass 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 single
definició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 viewModel
definición le dice a Koin que cree solo un tipo de objeto que amplíe la ViewModel
clase de Android.
Necesitamos inicializar Koin para preparar nuestras dependencias antes de que nuestra aplicación las solicite. Crea una Application
clase, 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.
Storage<Config>
está seguro de recuperar solo los datos de Config
tipoPreferenceHelper
Las 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ódigoPersistentStorage<T>
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/