服务注册中心
服务配置
服务总线
服务调用
服务降级
服务网关
https://github.com/alibaba/nacos/releaseshttps://nacos.io/zh-cn/docs/quick-start.htmlhttps://spring-cloud-alibaba-group.github.io/github-pages/hoxton/en-us/index.html
# 安装unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gzcd nacos/bin# 打开sh startup.sh -m standalone# 关闭sh shutdown.sh
link:http://127.0.0.1:8848/nacos/默认账号和密码为:nacos/nacos
注意:Nacos Server 的数据源是用 Derby 还是 MySQL 完全是由其运行模式决定的
parent pom
<properties> <spring.cloud.alibaba>2.2.1.RELEASE</spring.cloud.alibaba> </properties> <dependencyManagement> <dependencies> <!-- Spring Cloud Alibaba --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
provider/consumer client pom
<!-- 服务发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
provider
server: port: 7010 spring: application: name: nacos-provider cloud: nacos: discovery: server-addr: localhost:8848 # ${NACOS_SERVER:8848}
main
@SpringBootApplication @EnableDiscoveryClient public class NacosProviderApp { public static void main(String[] args){ SpringApplication.run(NacosProviderApp.class,args); } @RestController public class HelloController { @GetMapping("/hi") public Object hello(){ return "Hello";} } }
consumer
server: port: 7020 spring: application: name: nacos-consumer cloud: nacos: discovery: server-addr: localhost:8848 # ${NACOS_SERVER:8848}
main
@SpringBootApplication @EnableDiscoveryClient public class NacosConsumerApp { public static void main(String[] args){ SpringApplication.run(NacosConsumerApp.class,args); } @Bean @LoadBalanced // 使用serviceId访问则一定要加上,否则找不到服务;不加此注解,则可通过普通的ip:host的URL来访问服务 public RestTemplate restTemplate(){ return new RestTemplate(); } @RestController public class HelloController { private String remoteServiceId="nacos-provider"; @Autowired private RestTemplate restTemplate; @GetMapping("/") public Object index(){ return "Index"; } @GetMapping("/hi") public Object hello(){ String url = String.format("http://%s/hi",remoteServiceId); String result = restTemplate.getForObject(url,String.class); return "Say: "+result; } } }
Test
provider/consumer client pom
<!-- 服务配置 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- spring cloud 2020,需加上这个才能读取bootstrap.yml,Nacos Config才能正常使用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
bootstrap.yml (或者 bootstrap.properties) 优先级高于application.yml,nacos相关配置需配在此处
server: port: 7010 spring: application: name: nacos-provider cloud: nacos: discovery: server-addr: localhost:8848 # ${NACOS_SERVER:8848} | 注意:必须有端口号 config: server-addr: localhost:8848 file-extension: yaml # 支持properties(默认),yaml | 注意:nacos识别yaml,yml会报错 namespace: dear-v1 # 命名空间ID,不是命名空间名称 | 注意:只能配置一个命名空间 group: NACOS_GROUP # 默认DEFAULT_GROUP | 注意:只能配置一个GROUP # => 加载的dataId为: ${prefix}-${spring.profiles.active}.${file-extension} # prefix 默认为 spring.application.name # spring.profiles.active为空时 => 对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension} ext-config: # 列表,优先级都低于上面正常group&namespace下的配置,列表的优先级是下标越大优先级越高 + data-id: logback-spring.yaml group: LOG_GROUP refresh: true + data-id: external.yaml group: EXTERNAL_GROUP refresh: true
application.yml (optional): spring.profiles.active
可在运行时指定
spring: profiles: active: dev
Controller
@RestController @RefreshScope // 实现配置自动更新 public class ConfigController{ @Value("${provider.name:UnKnow}") private String providerName; @Value("${welcome:UnKnow}") private String welcome; @GetMapping("/config") public Object config(){ return "ProviderName: "+providerName +"; Welcome: "+welcome; } }
Nocos 配置界面:http://127.0.0.1:8848/nacos
,添加配置文件
welcome: External Welcome V1
logging: config: classpath:logback-spring-${spring.profiles.active}.xml path: logs package: com.cj.dear
provider: name: Default Nacos Provider
provider: name: Dev Nacos Provider
provider: name: Prod Nacos Provider
启动运行,访问 http://localhost:7010/config
--spring.profiles.active=prod
java -jar nacos-provider.jar --spring.profiles.active=dev
Sleuth 帮助记录这些traceID,spanIDZipkin Twitter的一个开源项目,收集链路的跟踪数据,提供可插拔的数据存储方式,UI直观查看分析
https://zipkin.io/https://github.com/openzipkin/openzipkin.github.iohttps://github.com/openzipkin/zipkin
核心组件:
分两端:
java -jar zipkin.jar
或使用docker docker run -d -p 9411:9411 openzipkin/zipkin
http://localhost:9411
pom.xml (所有微服务)
<!-- 链路追踪 Sleuth--> <!--<dependency>--> <!--<groupId>org.springframework.cloud</groupId>--> <!--<artifactId>spring-cloud-starter-sleuth</artifactId>--> <!--</dependency>--> <!-- Zipkin Client(dependence 已包含了sleuth相关依赖) 收集链路信息,发送给Zipkin Server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
application.xml (所有微服务)
spring: zipkin: base-url: http://localhost:9411 # Zipkin Server端地址 sender: type: web # 数据用HTTP方式传送给Zipkin Server端的方式 sleuth: sampler: probability: 0.5 # 数据采样比(0~1,默认0.1)
Test (多请求一些链接,然后去 http://localhost:9411 查看可视化界面)
https://github.com/openzipkin/zipkin/tree/master/zipkin-server
Sample: 持久化到MySQL
准备数据库表
重启 Zipkin Server, 启动时传递相关配置参数
java -jar zipkin.jar --STORAGE_TYPE=mysql --MYSQL_HOST=localhost --MYSQL_TCP_PORT=3306 --MYSQL_USER=cj --MYSQL_PASS=123 --MYSQL_DB=dear_zipkin
Sample: 使用消息中间件
准备RabbitMQ服务器 http://localhost:15672
修改Zipkin Server,从RabbitMQ中拉取信息
java -jar zipkin.jar --STORAGE_TYPE=mysql --MYSQL_HOST=localhost --MYSQL_TCP_PORT=3306 --MYSQL_USER=cj --MYSQL_PASS=123 --MYSQL_DB=dear_zipkin --RABBIT_ADDRESSES=localhost:5672 --RABBIT_USER=admin --RABBIT_PASSWORD=admin --RABBIT_VIRTUAL_HOST=my_vhost
修改微服务Zipkin client 相关配置,将采集信息发送RabbitMQ
服务调用(同类实现:Ribbon,LoadBalancer,Feign)
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
pom.xml
<!-- 服务发现 Nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <!-- 排除ribbon, 使用Springcloud loadbalancer (f否则启动报错)--> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency> <!-- Spring Cloud OpenFeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
application.yml
server: port: 7020 spring: application: name: nacos-consumer cloud: nacos: discovery: server-addr: localhost:8848 # ${NACOS_SERVER:8848} namespace: dear-v1 loadbalancer: ribbon: enabled: false # config feign -- optional feign: client: config: nacos-provider: # FeignClient名 或使用 default connectTimeout: 5000 readTimeout: 5000 loggerLevel: FULL logging: level: com.cj.dear.nacos: debug config: classpath:logback-spring-dev.xml path: logs
激活Feign @EnableFeignClients
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class NacosConsumerApp { public static void main(String[] args){ SpringApplication.run(NacosConsumerApp.class,args); } }
FeignClient (注意:feign要求指明确定返回值类,才能正确解析方法返回值,即方法返回值不能使用Object!)
@FeignClient("nacos-provider") public interface ProviderFeignClient{ @GetMapping("/hi") String hello(); @GetMapping("/config") String config(); }
Usage
@RestController public class HelloController{ @Autowired private ProviderFeignClient providerFeignClient; @GetMapping("callProvider") public Object callProvider(@RequestParam(required = false, name="func",defaultValue = "hi")String func){ System.out.println("func:"+func); if(func.equals("hi")) return "Call "+func+" Result: "+providerFeignClient.hello(); return "Call "+func+" Result: "+providerFeignClient.config(); } }
Test (启动N个nacos-provider)
服务降级(同类实现:Hystrix,Resilience4j)
https://github.com/alibaba/Sentinel/
https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
高并发问题由于请求积压,造成服务瘫痪,服务与服务之间存在依赖性,故障传播,连锁反应,造成整个系统崩溃 (雪崩)=》 处理方案:
https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0
启动Sentinel控制台
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
-Dcsp.sentinel.dashboard.server=consoleIp:port
指定控制台地址和端口。-Dcsp.sentinel.api.port=xxxx
指定客户端监控 API 的端口(默认是 8719)visit: http://localhost:8080/
pom.xml
<!-- Spring Cloud Aalibaba Sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
application.yml
spring: cloud: sentinel: transport: dashboard: localhost:8080 eager: true # 默认false,懒加载
java
@GetMapping("callProvider") @SentinelResource(value="callProvider", blockHandler="callProviderBlockHandler",fallback="callProviderFallbackHandler") public Object callProvider(@RequestParam(required = false, name="func",defaultValue = "hi")String func){ System.out.println("func:"+func); if(func.equals("hi")) return "Call "+func+" Result: "+providerFeignClient.hello(); return "Call "+func+" Result: "+providerFeignClient.config(); } // sentinel // 1. 限流熔断 blockHandler(捕获BlockException) 注意:参数(最后Optional: BlockException),返回值类型需与原方法一致 // 2. 异常降级 fallback(捕获其他异常) 注意:参数,返回值类型需与原方法一致(Optional: 可加Throwable) public Object callProviderBlockHandler(String func,BlockException ex){ return "CallProvider:"+func+"; BlockHandler"; } public Object callProviderFallbackHandler(String func){ return "CallProvider:"+func+"; FallBack"; }
启动服务
访问Sentinel控制台,配置限流规则,测试
一条限流规则的组成:
Sample: 本地文件加载限流规则
resources/flowRule.json
[ { "resource": "callProvider", "limitApp": "default", "grade": 0, "count": 2, "strategy": 0, "controlBehavior": 0 } ]
resources/application.yml
spring: application: name: nacos-consumer cloud: sentinel: transport: dashboard: localhost:8080 eager: true # 默认false,懒加载 datasource: ds1: file: # 配置从本地文件读取限流规则 dataType: json ruleType: flow file: classpath:flowRule.json
启动服务
visit sentinel控制台查看规则 http://localhost:8080/
Sample: 从nacos加载限流规则
visit nacos控制台,配置 nacos-consumer-sentinel.json
pom.xml: 增加 sentinel-datasource-nacos
包
<!-- Spring Cloud Aalibaba Sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!-- Sentinel从nacos获取限流规则--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
application.yml
spring: application: name: nacos-consumer cloud: sentinel: transport: dashboard: localhost:8080 eager: true # 默认false,懒加载 datasource: ds1: # file: # 配置从本地文件读取限流规则 # dataType: json # ruleType: flow # file: classpath:flowRule.json nacos: # 配置从nacos中获取 dataType: json ruleType: flow serverAddr: localhost:8848 namespace: dear-v1 dataId: nacos-consumer-sentinel.json groupId: DEFAULT_GROUP
启动服务
visit sentinel控制台查看规则 http://localhost:8080/
服务网关(同类实现:Zuul1,Zuul2)
可应用解决问题:
核心概念:
pom.xml
<!-- Spring Cloud Gateway | 注意:依赖WebFlux,所以不要引入spring-boot-starter-web,,不然会与SpringMVC冲突,启动报错--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-web</artifactId>--> <!--</dependency>--> <!-- Spring Cloud 2x 有这个问题 解决报错:Parameter 0 of method loadBalancerWebClientBuilderBeanPostProcessor in org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration required a bean of type 'org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction' that could not be found. --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
application.yml
spring: profiles: active: dev cloud: gateway: discovery: # 简化路由配置,自动根据serviceId进行路由转发 eg: http://localhost:81/nacos-consumer/hi 会自动转发到 http://nacos-consumer/hi locator: enabled: true # 开启根据serviceId自动转发|打开后,可使用serviceId,负载均衡定位访问服务 lower-case-service-id: true # 微服务名称以小写形式呈现 routes: + id: baidu uri: https://www.baidu.com filters: + SetPath=/?{args} predicates: + Path=/baidu/{args} + id: nacos-provider # 路由的ID,没有固定规则但要求唯一 uri: lb://nacos-provider/ # 匹配后提供服务的路由地址(lb:// 根据serviceId,负载均衡策略确定具体服务地址) filters: + StripPrefix=1 # 过滤器,不加时定位服务为:lb://nacos-provider/provider/**;设置去掉第一段,则变成lb://nacos-provider/** predicates: + Path=/provider/** # 断言,路径相匹配的进行路由
自定义GlobalFilter
@Component public class GatewayTokenFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if(StringUtils.isEmpty(token)){ ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.FORBIDDEN); String msg = "Gateway:Forbidden to visit"; DataBuffer df = response.bufferFactory().wrap(msg.getBytes()); return response.writeWith(Mono.just(df)); } return chain.filter(exchange); } }
Test
结合Nacos
server: port: 81 spring: application: name: cloud-gateway cloud: nacos: discovery: server-addr: localhost:8848 namespace: dear-v1 config: server-addr: localhost:8848 file-extension: yaml namespace: dear-v1 group: CLOUD_GROUP ext-config: + data-id: logback-spring.yaml group: LOG_GROUP refresh: true + data-id: external.yaml group: EXTERNAL_GROUP refresh: true
main
@SpringBootApplication @EnableDiscoveryClient public class CloudGatewayApp { public static void main(String[] args){ SpringApplication.run(CloudGatewayApp.class,args); } @Value("${provider.name:UnKnow}") private String providerName; @RestController @RefreshScope public class HelloController{ @Value("${provider.name:UnKnow}") private String providerName; @GetMapping("/hi") // http://localhost:81/hi => Gateway: Hello | ProviderName: Default Cloud Gateway V5 dev public String Hello() { return "Gateway: Hello | ProviderName: "+providerName; } } }
Test (需启动 Nacos Server,nacos-provider工程)
RoutePredicateFactory
spring.cloud.gateway.discovery.locator.enable=true
spring: cloud: gateway: discovery: # 简化路由配置,自动根据serviceId进行路由转发 locator: enabled: true # 开启根据serviceId自动转发|打开后,可使用serviceId,负载均衡定位访问服务 lower-case-service-id: true # 微服务名称以小写形式呈现
开启后,无需配置routes,即可实现visit http://localhost:81/nacos-consumer/hi
自动转发到 http://nacos-consumer/hi
Test: http://localhost:81/nacos-consumer/hi?token=11 =》 Say: Hello
pre
post
GatewayFilter
局部过滤器,应用到单一路由,或一个分组路由上GatewawyFilterFactory
结尾)GlobalFilter
全局过滤器,应用到所有路由上,eg 内置的一些全局过滤器自定义GlobalFilter: 实现GlobalFilter, Ordered(Optional,越小优先级越高)
常见限流算法:
Spring Cloud Gateway:
RequestRateLimiterGatewayFilterFactory
实现(通过Redis和lu脚本结合的方式实现)pom.xml
<!-- 网关限流: 基于Filter 实现 --> <!-- 监控依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Reids 相关依赖(基于reactive的redis依赖)) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
application.yml 添加redis & 限流配置
spring: redis: host: localhost port: 6379 database: 1 password: 123456 cloud: gateway: routes: + id: nacos-provider uri: lb://nacos-provider/ filters: + name: RequestRateLimiter # 使用限流过滤器 args: key-resolver: '#{@queryParamKeyResolver}' # 限流Key解析器(即基于什么限流),使用SpEL表达式@xxx从Spring容器中获取name为xxx的Bean对象, 查看自定义的RateLimiteConfig中注入的Bean redis-rate-limiter.replenishRate: 1 # 向令牌桶中填充token的速率 redis-rate-limiter.burstCapacity: 3 # 令牌桶容量 redis-rate-limiter.requestedTokens: 1 # 一个请求消耗几个令牌 + StripPrefix=1 predicates: + Path=/provider/**
编写KeyResolver
@Configuration public class KeyResolverConfig { // 基于请求路径限流 @Bean @Primary public KeyResolver pathKeyResolver(){ return new KeyResolver(){ @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getPath().toString()); } }; } // 基于请求参数限流 @Bean public KeyResolver queryParamKeyResolver(){ return exchange ->{ String token = exchange.getRequest().getQueryParams().getFirst("token"); return Mono.just(token!=null?token:exchange.getRequest().getPath().toString()); }; } }
Test
> select 1 > monitor
https://www.cnblogs.com/yinjihuan/p/10772558.html
https://github.com/alibaba/Sentinel/wiki/API-Gateway-Flow-Control
pom.xml
<!-- 网关限流: 基于Sentinel 实现 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency>
application.yml
spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: + id: baidu uri: https://www.baidu.com filters: + SetPath=/?{args} predicates: + Path=/baidu/{args} + id: nacos-provider uri: lb://nacos-provider/ predicates: + Path=/provider/** filters: + StripPrefix=1
Configuration
@Configuration public class SentinelGatewayConfig { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public SentinelGatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } // 配置限流异常处理器 @Bean @Order(-1) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } // 配置限流过滤器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { // By default the order is HIGHEST_PRECEDENCE return new SentinelGatewayFilter(); } // Optinal: 配置一些初始化的限流规则 // Test: http://localhost:81/provider/hi?token=22 @PostConstruct public void initGatewayRules(){ Set<GatewayFlowRule> rules = new HashSet<>(); // 路由ID,单位时间限流阈值,单位时间 // rules.add(new GatewayFlowRule("nacos-provider").setCount(2).setIntervalSec(1)); // 限流分组名,单位时间限流阈值,单位时间 rules.add(new GatewayFlowRule("providerApis").setCount(2).setIntervalSec(1)); GatewayRuleManager.loadRules(rules); } // Optinal: 自定义限流分组 @PostConstruct public void initCustomizedApis(){ Set<ApiDefinition> definitions = new HashSet<>(); Set<ApiPredicateItem> predicateItems = new HashSet<>(); predicateItems.add(new ApiPathPredicateItem() .setPattern("/provider/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX) ); ApiDefinition def = new ApiDefinition("providerApis") .setPredicateItems(predicateItems); definitions.add(def); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } // Optinal: 自定义限流处理器 @PostConstruct public void initBlockHandlers(){ BlockRequestHandler handler = new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap(); map.put("code","001"); map.put("message","不好意思,限流了哦"); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(map)); } }; GatewayCallbackManager.setBlockHandler(handler); } }
Ngnix + 网关集群
Ngnix 配置
// gateway集群 upstream gateway{ server 127.0.0.1:81; server 127.0.0.1:82; } Server{ listen 80; server_name localhost; # 路由 location / { proxy_pass http://gateway; } }
启动两个网关实例,端口分别为81,82
OAuth2.0 是一种协议,提供认证和授权的标准SpringSecurity,Shiro等框架有自己的实现
OAuth2.0 涵盖两个服务,可放于一个应用程序中实现,也可用于 1(认证服务)+ N(资源服务)分别部署
/auth/authorize
/auth/token
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer { @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { }}
需配置以下三个对象:
认证授权模式:
客户端模式 client_credentials
认证:验证账号密码
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); }}
ResourceServerTokenServices 验证token
https://spring.io/projects/spring-security-oauthhttps://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guidehttps://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
application.yml
server: port: 5000 servlet: context-path: /dear-auth
配置OAuth认证授权服务器
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{ /// 配置Client @Autowired private PasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 使用内存方式 clients.inMemory() .withClient("DearApp")//客户端id .secret(passwordEncoder.encode("DearApp")) .authorizedGrantTypes("authorization_code", "password","refresh_token", ) // 该客户端允许的授权类型(authorization_code,password,client_credentials,implicit,refresh_token) .scopes("app")//允许的授权范围,名称自定义,是个标识,必填 .redirectUris("http://www.baidu.com") //验证回调地址 ; } }
配置Security(提供认证授权页面进行认证: http://localhost:5000/dear-auth/login
)
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true) class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 采用bcrypt对密码进行编码 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN") .and() .withUser("user").password(passwordEncoder().encode("123")).roles("USER") ; } }
AuthApp.java
@SpringBootApplication public class AuthApp { public static void main(String[] args){ SpringApplication.run(AuthApp.class); } }
测试:
http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code
http://localhost:5000/dear-auth/login
,输入账户密码(admin,123)进行验证http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code
,选择是否同意授权https://www.baidu.com/?code=3Tgl8q
,即获得授权码3Tgl8q
获取token
使用cmd命令:
> curl -d 'grant_type=authorization_code&code=3Tgl8q' -X POST http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token -i HTTP/1.1 200 Cache-Control: no-store Pragma: no-cache X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block X-Frame-Options: DENY Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 21 Jan 2021 13:32:58 GMT {"access_token":"e775d74e-9e8c-437d-974d-6d8286b37396","token_type":"bearer","refresh_token":"6b9f3fa3-143b-47a0-b356-3f577e8c7fed","expires_in":43199,"scope":"app"}
http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token
AuthorizationServerConfig
配置了public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients(); }
,则表示可以使用表单认证,可不用Basic Auth,直接在body中再加入client_id & client_secret
参数数据库 https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
/* SQLyog v12.2.6 (64 bit) MySQL - 8.0.15 : Database - dear_v1_auth ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`dear_v1_auth` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `dear_v1_auth`; /*Table structure for table `clientdetails` */ DROP TABLE IF EXISTS `clientdetails`; CREATE TABLE `clientdetails` ( `appId` varchar(128) NOT NULL, `resourceIds` varchar(256) DEFAULT NULL, `appSecret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `grantTypes` varchar(256) DEFAULT NULL, `redirectUrl` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additionalInformation` varchar(4096) DEFAULT NULL, `autoApproveScopes` varchar(256) DEFAULT NULL, PRIMARY KEY (`appId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_access_token` */ DROP TABLE IF EXISTS `oauth_access_token`; CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, `authentication` blob, `refresh_token` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_approvals` */ DROP TABLE IF EXISTS `oauth_approvals`; CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` timestamp NULL DEFAULT NULL, `lastModifiedAt` timestamp NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_client_details` */ DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(128) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_client_token` */ DROP TABLE IF EXISTS `oauth_client_token`; CREATE TABLE `oauth_client_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_code` */ DROP TABLE IF EXISTS `oauth_code`; CREATE TABLE `oauth_code` ( `code` varchar(256) DEFAULT NULL, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Table structure for table `oauth_refresh_token` */ DROP TABLE IF EXISTS `oauth_refresh_token`; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
插入client数据 table: oauth_client_details
配置数据源
pom.xml
<!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!-- for @ConfigurationProperties : optional ! --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
spring: datasource: druid: url: jdbc:mysql://localhost:3306/dear_v1_auth?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowMultiQueries=true username: cj password: 123 driver-class-name: com.mysql.cj.jdbc.Driver # type: com.alibaba.druid.pool.DruidDataSource initial-size: 8 min-idle: 1 max-active: 20 max-wait: 60000 time-between-eviction-runsMillis: 60000 min-evictable-idle-timeMillis: 300000 validation-query: select 'x' test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: false max-open-prepared-statements: 20 max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall use-global-data-source-stat: true connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
config/DruidConfig
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DruidConfig { //https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter @ConfigurationProperties(prefix = "spring.datasource.druid") @Bean public DataSource druidDataSource(){ return new DruidDataSource(); } }
配置授权认证服务器: client & token 存储到数据库
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{ // 1. 配置 client @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(jdbcClientDetailsService()); } @Autowired DataSource dataSource; @Bean public ClientDetailsService jdbcClientDetailsService(){ return new JdbcClientDetailsService(dataSource); } // 2. 配置 endpoints,token存储 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(jdbcTokenStore()); } @Bean public TokenStore jdbcTokenStore(){ return new JdbcTokenStore(dataSource); } }
/* SQLyog v12.2.6 (64 bit) MySQL - 8.0.15 : Database - dear_v1_auth ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`dear_v1_auth` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `dear_v1_auth`; /*Table structure for table `tb_permission` */ DROP TABLE IF EXISTS `tb_permission`; CREATE TABLE `tb_permission` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限', `name` varchar(64) NOT NULL COMMENT '权限名称', `enname` varchar(64) NOT NULL COMMENT '权限英文名称', `url` varchar(255) NOT NULL COMMENT '授权路径', `description` varchar(200) DEFAULT NULL COMMENT '备注', `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8 COMMENT='权限表'; /*Data for the table `tb_permission` */ insert into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values (37,0,'系统管理','System','/',NULL,'2019-04-04 23:22:54','2019-04-04 23:22:56'), (38,37,'用户管理','SystemUser','/users/',NULL,'2019-04-04 23:25:31','2019-04-04 23:25:33'), (39,38,'查看用户','SystemUserView','/users/view/**',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'), (40,38,'新增用户','SystemUserInsert','/users/insert/**',NULL,'2019-04-04 15:30:31','2019-04-04 15:30:44'), (41,38,'编辑用户','SystemUserUpdate','/users/update/**',NULL,'2019-04-04 15:30:32','2019-04-04 15:30:45'), (42,38,'删除用户','SystemUserDelete','/users/delete/**',NULL,'2019-04-04 15:30:48','2019-04-04 15:30:45'), (44,37,'内容管理','SystemContent','/contents/',NULL,'2019-04-06 18:23:58','2019-04-06 18:24:00'), (45,44,'查看内容','SystemContentView','/contents/view/**',NULL,'2019-04-06 23:49:39','2019-04-06 23:49:41'), (46,44,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2019-04-06 23:51:00','2019-04-06 23:51:02'), (47,44,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2019-04-06 23:51:04','2019-04-06 23:51:06'), (48,44,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2019-04-06 23:51:08','2019-04-06 23:51:10'); /*Table structure for table `tb_role` */ DROP TABLE IF EXISTS `tb_role`; CREATE TABLE `tb_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色', `name` varchar(64) NOT NULL COMMENT '角色名称', `enname` varchar(64) NOT NULL COMMENT '角色英文名称', `description` varchar(200) DEFAULT NULL COMMENT '备注', `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表'; /*Data for the table `tb_role` */ insert into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values (37,0,'超级管理员','admin',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05'); /*Table structure for table `tb_role_permission` */ DROP TABLE IF EXISTS `tb_role_permission`; CREATE TABLE `tb_role_permission` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `role_id` bigint(20) NOT NULL COMMENT '角色 ID', `permission_id` bigint(20) NOT NULL COMMENT '权限 ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8 COMMENT='角色权限表'; /*Data for the table `tb_role_permission` */ insert into `tb_role_permission`(`id`,`role_id`,`permission_id`) values (37,37,37), (38,37,38), (39,37,39), (40,37,40), (41,37,41), (42,37,42), (43,37,44), (44,37,45), (45,37,46), (46,37,47), (47,37,48); /*Table structure for table `tb_user` */ DROP TABLE IF EXISTS `tb_user`; CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(64) NOT NULL COMMENT '密码,加密存储', `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号', `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱', `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) USING BTREE, UNIQUE KEY `phone` (`phone`) USING BTREE, UNIQUE KEY `email` (`email`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表'; /*Data for the table `tb_user` */ insert into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values (37,'admin','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','15888888888','lee.lusifer@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29'); /*Table structure for table `tb_user_role` */ DROP TABLE IF EXISTS `tb_user_role`; CREATE TABLE `tb_user_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户 ID', `role_id` bigint(20) NOT NULL COMMENT '角色 ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表'; /*Data for the table `tb_user_role` */ insert into `tb_user_role`(`id`,`user_id`,`role_id`) values (37,37,37); /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
配置MyBatis: application.yml,main上加@MapperScan("com.cj.dear.auth.mapper")
注解
mybatis: mapper-locations: classpath:mapper/*Mapper.xml config-location: classpath:mybatis-config.xml
编写entity,mapper,service
config/WebSecurityConfig.java
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true) class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 采用bcrypt对密码进行编码 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired UserDetailsServiceImpl userDetailsServiceImpl; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsServiceImpl); } }
UserDetailsServiceImpl.java
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private TbUserService tbUserService; @Autowired private TbPermissionService tbPermissionService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { TbUser tbUser = tbUserService.getByUsername(username); if(tbUser!=null){ List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); List<TbPermission> tbPermissions = tbPermissionService.listPermisionsByUserId(tbUser.getId()); if(tbPermissions!=null){ tbPermissions.forEach(tbPermission -> { grantedAuthorityList.add(new SimpleGrantedAuthority(tbPermission.getEnname())); }); } return new User(tbUser.getUsername(),tbUser.getPassword(),grantedAuthorityList); } return null; } }
pom.yml
<!-- OAuth --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
application.yml
security: oauth2: client: client-id: DearApp client-secret: DearApp access-token-uri: http://localhost:5000/dear-auth/oauth/token user-authorization-uri: http://localhost:5000/dear-auth/authorize resource: token-info-uri: http://localhost:5000/dear-auth/oauth/check_token
config/ResourceServerConfig
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/").hasAuthority("SystemProject") .antMatchers("/projects/view/**").hasAuthority("SystemProjectView") ; } }
ProjectController
@RestController public class ProjectController { @GetMapping("/") public Object index(){ return "Project Hello World"; } @GetMapping("/projects/view") public Object listProjects(){ return "List Projects"; } @GetMapping("/projects/insert") public Object insertProject(){ return "Insert Projects"; } }
visit http://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
=> Exception (springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2021-01-22T15:01:31.342+00:00","status":403,"error":"Forbidden","message":"","path":"/dear-auth/oauth/check_token"}]
)
http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
成功 /// Sample 3: /oauth/check_token 401/403 配置这个或者在AuthorizationServerConfig中配置security.checkTokenAccess策略 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/check_token"); }
http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
+ Basic Auth 成功 /// Sample 3: 此认证服务器的安全策略 @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") // /oauth/token_key 完全开放 .checkTokenAccess("isAuthenticated()") // /oauth/check_token 需要认证通过,可采用http basic认证 .allowFormAuthenticationForClients() // 允许表单认证 ; }
test
http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code
=> 获取code:nfGdkghttp://localhost:5000/dear-auth/oauth/token
=> 获取access_token: 1e9aedd1-4e92-4444-96a7-edc3b55a3d25http://localhost:5001/dear-project/
=> unauthorizedhttp://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
=> Success!http://localhost:5001/dear-project/
+ Bearer Token => Success!http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
=> { "active": true, "exp": 1611353145, "user_name": "admin", "authorities": [ "SystemProjectView", "SystemProject", "SystemUserView", "SystemProjectInsert", "SystemProjectUpdate", "SystemUser", "SystemUserInsert", "SystemUserDelete", "SystemUserUpdate", "System", "SystemProjectDelete" ], "client_id": "DearApp", "scope": [ "app" ] }
POST http://localhost:5000/dear-auth/oauth/token
curl -d 'grant_type=authorization_code&code=3Tgl8q' -X POST http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token -i
GET http://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25
SpringSecurityOAuth2登录后无法跳转获取授权码地址,直接跳转根路径原因详解https://blog.csdn.net/CSDN877425287/article/details/110948221
SpringBoot集成SpringSecurity - 异常处理(三)https://www.jianshu.com/p/5b412418b864/
SpringBoot2.x统一异常捕获@RestControllerAdvicehttps://blog.csdn.net/fuu123f/article/details/107249708
Spring Security OAuth2 授权失败(401) 问题整理https://www.cnblogs.com/mxmbk/p/9782409.html
SpringBoot /error Error Page status 为 999 的问题https://learnku.com/java/t/39683
Spring Cloud OAuth2 实现用户认证及单点登录https://www.cnblogs.com/fengzheng/p/11724625.html
官方 spring-security-oauth2https://github.com/spring-projects/spring-security-oauth/tree/master/spring-security-oauth2
]]>笔记:
String str1 = 'Hello';String str2 = "It's me";String str3 = """HelloWorld""";print("$str1,$str2,$str3");var name = "Tom";var age = 10;var height = 120;var msg1 = "my name is ${name}, age is ${age}, height is ${height}";var msg2 = "name is ${name}, type is ${name.runtimeType}";print(msg1);print(msg2);
int
,double
可表示的范围并不是固定的,取决于运行Dart的平台String
可使用${expression}
实现字符串和其他变量或表达式拼接,${变量}
,则{}
可省略,即 $exp
布尔类型: bool
取值为true/false,没有非0/非空即true的规则,即不能使用if(非布尔类型的变量)
,assert(非布尔类型的变量)
之类的代码
var flag = true; if(flag){ // right !! print(flag); } var message="Hello"; if(message){ // wrong !! print(message); }
集合类型: List
/Set
/Map
var nameList = ["Tom", "Lucy", "Susan"]; nameList.add("Jack"); nameList.remove("Lucy"); print( "$nameList, len: ${nameList.length}, contain Tom: ${nameList.contains('Tom')}"); var movieSet = {"a", "b", "c"}; print( "$movieSet,len: ${movieSet.length}, contain b: ${movieSet.contains('b')}"); var infoMap = {"name": "Tom", "age": 18}; print( "$infoMap,entries: ${infoMap.entries},keys: ${infoMap.keys},infoMap['age']=${infoMap['age']}"); // 去重: var charList = ['a', 'b', 'b', 'c', 'a']; var noDupCharList = Set<String>.from(charList).toList(); print( "charList:$charList, set: ${Set.from(charList)}, dup removed list: ${Set.from(charList).toList()}, noDupCharList:$noDupCharList");
类:class
(dart 中是没有关键字来定义接口的,默认所有的class都是隐式接口)
class Person { String name; Person(String name) { this.name = name; } } class SingletonPerson { final String name; const SingletonPerson(this.name); }
var
/ final
/ const
): 虽然没有明确指定变量类型,但变量还是有自己明确的类型的 var score = 20; // 如果后面再设置 ` score="abc"; ` 会报错 score = 30;
声明常量 final / const
// final 运行期间确定一个值,即可以通过计算/函数获取一个值 // const 编译期间确定一个值,即须直接赋予一个值 final date = DateTime.now(); // 正确 const date = DateTime.now(); // 错误 final price = 1.88; // 如果后面再设置 ` price=2.00; ` 会报错 const address = "SuZhou"; // 如果后面再设置 ` address="ShangHai"; ` 会报错 final p1 = Person("Tom"); // = new Person("Tom"); final p2 = Person("Tom"); // = new Person("Tom"); print(identical(p1, p2)); // => false p1和p2是不同的对象 const s1 = SingletonPerson("Tom"); // = const SingletonPerson("Tom"); const s2 = SingletonPerson("Tom"); // = const SingletonPerson("Tom"); const s3 = SingletonPerson("Lucy"); // = const SingletonPerson("Lucy"); print(identical(s1, s2)); // => true s1和s2是同一个对象 print(identical(s1, s3)); // => false s1和s3不是同一个对象
// 注: 关键字(var/const/final/...) vs 类型 (String,Object,dynamic,...)Object a = "Hello";// print("$a,${a.substring(3)}"); // 无法调用 String的substring 方法,编译时即报错了dynamic b = "Hello";print("$b, ${b.substring(3)}"); // 可调用 String的substring 方法,编译时不报错,但存在安全隐患,可能调用了一个不存在的方法,运行时报错//print("$b, ${b.say()}"); // 编译时不报错,运行时报错var c = "Hello";print("$c,${c.substring(3)}"); // 可调用 String的substring 方法// print("$c,${c.say()}"); // 编译时就报错了
函数的 必选参数 & 可选参数(位置可选参数,命名可选参数)注:dart中无函数重载,即不可以声明两个同名函数
必选参数(必传,且无法设置默认参数)
void sayHello1(String name) { print(name); } sayHello1("Tom");
可选参数
位置可选参数 []
void sayHello2(String name, [int age, double height]) { print("$name,$age,$height"); } sayHello2("Lucy"); // Lucy,null,null sayHello2("Lucy", 18); // Lucy,18,null sayHello2("Lucy", 18, 165.5); // Lucy,18,165.5
命名可选参数 {}
void sayHello3(String name, {int age, double height}) { print("$name,$age,$height"); } sayHello3("Lucy"); // Lucy,null,null sayHello3("Lucy", height: 165.5); // Lucy,null,165.5 sayHello3("Lucy", age: 18, height: 165.5); // Lucy,18,165.5
可设置默认参数
void sayHello4(String name, {int age = 16, double height = 120}) { print("$name,$age,$height"); } sayHello4("Lucy", height: 165.5); // Lucy,16,165.5
函数是一等公民,即函数可以赋给一个变量,作为参数/返回值来回传递(在一般的面向对象的语言中,如Java,一般class/object是第一公民,可作为参数/返回值来回传递)
函数作为另一个函数的参数
直接使用Function -- 对函数无限制
void test1(Function foo) { foo(); // foo(20,30),...对函数无限制 } void bar([String name = "Hi"]) { print(name); } // 传入函数 - 直接使用某个定义的函数 test1(bar); // 传入函数 - 使用匿名函数 test1(() { print("test 匿名函数"); }); // 传入函数 - 使用箭头函数(这种函数体必须只有一行代码) test1(() => print("test 箭头函数"));
// void test2(int foo(int a, int b)) { // 太长,阅读性差 // var result = foo(20, 30); // print(result); // } typedef Calcuate = int Function(int a, int b); // 函数签名 void test2(Calcuate cal) { var result = cal(20, 30); print(result); }
函数作为返回值
Calcuate getTestFunc() { return (a, b) { return a + b; }; } Function getTestFunc2() { return (a, b) { return a + b; }; } // 函数作为返回值 var ts = getTestFunc(); print(ts(20, 30)); var ts2 = getTestFunc2(); print(ts2(20, 50));
??=
void test1([String name]) { name ??= "Lucy"; // name = (name!=null? name:"Lucy") 或等同于 [String name="Lucy"] print(name);}test1();test1("Tom");
??
void test2([String name]) { var temp = name ?? "Lucy"; // temp = ( name!=null? name:"Lucy") print(temp);}test2();test2("Tom");
::
级联运算符
class Person { String name; void run() { print("$name running"); } void eat() { print("$name eating"); }}main(List<String> args) { var p1 = Person(); p1.name = "Tom"; p1.run(); p1.eat(); print("---------------------"); var p2 = Person() ..name = "Lucy" ..run() ..eat();}
构造方法
Dart本身不支持函数的重载 => 创建相同名称的构造方法 => Solution: 命名构造方法
class Person {String name;int age;double height;// 1. 构造函数语法糖// Person(String name, int age) {// this.name = name;// this.age = age;// }// => 等同于:Person(this.name, this.age);// 2. 命名构造方法(名称可自定义)Person.withArguments(this.name, this.age, this.height);Person.fromMap(Map<String, dynamic> map) { this.name = map['name']; this.age = map['age']; this.height = map['height'];}// 3. 重定向构造函数:在一个构造方法中去调用另外一个构造方法,冒号后面使用this调用Person.fromName(String name) : this(name, 20);Person.fromName2(String name) : this.withArguments(name, 30, 166.8);// 重写toString方法@overrideString toString() { return "$name,$age,$height";}}main(List<String> args) {var p1 = Person("Tom", 18);var p2 = Person.withArguments("Lucy", 16, 165);// var p3 = Person.fromMap({"height": 170); // wrong !! Unhandled exception: type 'int' is not a subtype of type 'double'var p3 = Person.fromMap( {"age": 20, "name": "Susan", "height": 170.5, "sex": "female"});print("p1 : $p1 , p2 : $p2 , p3 : $p3 ");var p4 = Person.fromName("Kelly1");var p5 = Person.fromName2("Kelly2");print("p4 : $p4 , p5 : $p5");}
初始化列表(Initializer list)
class FinalPerson { final String name; final int age; // FinalPerson(this.name) { // this.age = 10; // wrong !! age是final类型,编译不通过 // } // => 正确写法: FinalPerson(this.name) : this.age = 10; //FinalPerson.withOptional(this.name, {this.age = 10}); // right !!,可使用表达式 FinalPerson.withOptional(this.name, {int age}) : this.age = age ?? 10; // 推荐,后面可跟多条语句,`,`分隔 // 重写toString方法 @override String toString() { return "$name,$age"; } } var fp1 = FinalPerson("Tom"); print("fp1 : $fp1"); var fp2 = FinalPerson.withOptional("Lucy"); print("fp2 : $fp2"); var fp3 = FinalPerson.withOptional("Susan", age: 20); print("fp3 : $fp3");
常量构造
/* 注意: 1. 拥有常量构造方法的类中,所有的成员变量必须是final修饰 2. 为了可以通过常量构造方法,创建出相同的对象,不再使用 new关键字,而是使用const关键字 (如果是将结果赋值给const修饰的标识符时,const可以省略) */ class ConstPerson { final String name; const ConstPerson(this.name); } // var cp1 = const ConstPerson("Jack"); // var cp2 = const ConstPerson("Jack"); // print(identical(cp1, cp2)); // => true const cp1 = ConstPerson("Jack"); // 等同于 var cp1 = const ConstPerson("Jack"); const cp2 = ConstPerson("Jack"); print(identical(cp1, cp2)); // => true
工厂构造 factory
可以手动返回一个对象(普通的构造函数不能手动返回)
class FactoryPerson { String name; String color; FactoryPerson(this.name, this.color); // eg 需求: 希望name或者color相同时返回的是同一个对象 static final Map<String, FactoryPerson> _nameCache = {}; static final Map<String, FactoryPerson> _colorCache = {}; factory FactoryPerson.withName(String name) { if (_nameCache.containsKey(name)) return _nameCache[name]; else { final p = FactoryPerson(name, "default"); _nameCache[name] = p; return p; } } factory FactoryPerson.withColor(String color) { if (_colorCache.containsKey(color)) return _colorCache[color]; else { final p = FactoryPerson("default", color); _colorCache[color] = p; return p; } } } var facP1 = FactoryPerson.withName("Mark"); var facP2 = FactoryPerson.withName("Mark"); print(identical(facP1, facP2)); // => true
Dart中类定义的属性是可以直接被外界访问的,但通过使用setter和getter,可以监控这个类的属性被访问的过程
// _xxx 下划线,当前模块中可使用class Person { String name; // setter - `set` // set setName(String name) { // this.name = name; // } set setName(String name) => this.name = name; // getter - `get` // String get getName { // return this.name; // } String get getName => this.name;}final p = Person();p.name = "Tom";print(p.name);p.setName = "Jack";print(p.getName);
继承类 extends
class Animal { int age; Animal(this.age); void eating() { print("Animal is eating"); } } class Cat extends Animal { String name; Cat(this.name, int age) : super(age); @override void eating() { print("$name is eating"); } } var c = Cat("Miao", 2); c.eating();
抽象类 abstract
//(不能实例化,但可通过factory构造函数实例化) // 参看 Map抽象类, 有个extenal factory Map(); // external: 将方法的声明和实现分离,用个注解@patch在其他地方对方法进行实现 => 好处: 针对不同平台,可有不同的实现 // map_patch.dart abstract class Shape { // 抽象方法 (无方法体) void getArea(); // 普通方法(有方法体) void getInfo() { print("Shape"); } } // Dart 只支持单继承,不能多继承,即只能extends一个类 class Rectangle extends Shape { // 必须实现抽象方法 @override void getArea() { print("Rectangle getArea!"); } } var s = Rectangle(); s.getArea();
接口 implements(Dart中,默认情况下所有类都是接口)
class Runner { void running() { print("Running"); } } // Dart中可以implements多个类,但必须重新实现@override其中的所有方法 class Wind implements Runner { @override void running() { print("Wind is running"); } } var w = Wind(); w.running();
混入类(Mixin定义类,with混入类)
// 可复用之前类的原有实现方案 mixin Swimer { void swiming() { print("Swiming"); } } mixin Flier { void flying() { print("Flying"); } } class SpaceShip with Swimer, Flier { @override void swiming() { print("SpaceShip can swiming!"); } } var ss = SpaceShip(); ss.flying(); ss.swiming();
/*常见属性: index: 用于表示每个枚举常量的索引, 从0开始 values: 包含每个枚举值的List.注意: 不能子类化、混合或实现枚举 不能显式实例化一个枚举 */enum Colors { red, green, blue }print(Colors.blue);print(Colors.green.index);print(Colors.values);
class Location<T extends num> { T x; T y; Location(this.x, this.y); @override String toString() { return "$x,$y"; }}T getListFirst<T>(List<T> list) { return list[0];}var names = ["Tom", "Lucy"];var first = getListFirst(names);print("$first,${first.runtimeType}");var ages = [20, 30];var a = getListFirst(ages);print("$a,${a.runtimeType}");print("---------------------------");var l1 = Location(10, 20);print(l1);var l2 = Location<int>(20, 30);print(l2);var l3 = Location<double>(20.5, 30.9);print(l3);//var l4 = Location<String>("20", "39"); // wrong !!
Dart中任何一个dart文件都是一个库,即使没有用libary声明(库拆分: part/export - 推荐使用export)
// mathUtils.dart: int sum(int num1, int num2) { return num1 + num2; } // dateUtils.dart: String dateFormat(DateTime date) { return "2020-12-12"; } // utils.dart: library utils; export "mathUtils.dart"; export "dateUtils.dart"; // test_libary.dart: import "lib/utils.dart"; main(List<String> args) { print(sum(10, 20)); print(dateFormat(DateTime.now())); }
导入库 import (Dart中任何一个dart文件都是一个库)
import '库所在的uri';
show
/hide
显示/隐藏某个成员 eg: import 'lib/student/student.dart' show Student, Person;as
命名空间,解决命名冲突 eg: import 'lib/student/student.dart' as Stu; Stu.Student s = new Stu.Student();https://www.imooc.com/article/305495
Future<String> testFuture() { // return Future.value("Hello"); return Future.error("haha 创造个error");// => onError会打印【代码中有onError回调,catchError不会执行】// throw "an Error"; // => 代码会直接异常,不会走 catchError方法,因为throw返回的数据类型不是Future<T>类型}testFuture().then((value){ print(value); },onError: (e) { // onError为可选参数,代码异常时会被调用【代码中有onError回调,catchError不会执行】 print("onError: $e"); }).catchError((e){ print("catchError: $e"); });// 类似于java中try{}catch(){}finally{}异常捕获的finallyFuture.error("haha make a error") .then(print) .catchError(print) .whenComplete(() => print("Done!!!"));//执行结果://haha make a error//Done!!!
testAsync() async { var result = await Future.delayed(Duration(milliseconds: 2000), ()=>Future.value("hahha")); print("time = ${DateTime.now()}"); print(result);}print("time start = ${DateTime.now()}");testAsync();print("time end= ${DateTime.now()}");//执行结果://time start = 2019-05-15 19:24:14.187961//time end= 2019-05-15 19:24:14.200480//time = 2019-05-15 19:24:16.213213//hahha
Future.delayed(Duration(milliseconds: 3000), () => "hate") .timeout(Duration(milliseconds: 2000)) .then(print) .catchError(print);//TimeoutException after 0:00:00.002000: Future not completed
]]>创建2D App的SDKDart语言构建的编程框架用一种语言编程(dart) => 为不同的平台定制(ios/android)=> Build & 发布
Flutter Dev | Flutter中文网 | Flutter Github
下载zip包解压 flutter_macos_1.20.4-stable
=> /Users/cj/soft/flutter
配置环境变量
> vi ~/.bash_profile # Flutter export FLUTTER_HOME=/Users/cj/soft/flutter export DART_HOME=$FLUTTER_HOME/bin/cache/dart-sdk # Flutter temp visit images (创建的flutter项目会使用这个镜像下载依赖) export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn # Path export PATH=$PATH:$FLUTTER_HOME/bin:$DART_HOME/bin > source ~/.bash_profile
> flutter --version Flutter 1.20.4 • channel stable • https://github.com/flutter/flutter.git Framework • revision fba99f6cf9 (9 days ago) • 2020-09-14 15:32:52 -0700 Engine • revision d1bc06f032 Tools • Dart 2.9.2 > dart --version Dart SDK version: 2.9.2 (stable) (Wed Aug 26 12:44:28 2020 +0200) on "macos_x64" > which flutter /Users/cj/soft/flutter/bin/flutter > which dart /Users/cj/soft/flutter/bin/dart
诊断依赖项
> flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 1.20.4, on Mac OS X 10.11.2 15C50, locale zh-Hans) [✗] Android toolchain - develop for Android devices ✗ Unable to locate Android SDK. Install Android Studio from: https://developer.android.com/studio/index.html On first launch it will assist you in installing the Android SDK components. (or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions). If the Android SDK has been installed to a custom location, set ANDROID_SDK_ROOT to that location. You may also want to add it to your PATH environment variable. [!] Xcode - develop for iOS and macOS (Xcode 7.2) ✗ Flutter requires a minimum Xcode version of 11.0.0. Download the latest version or update via the Mac App Store. ✗ CocoaPods not installed. CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side. Without CocoaPods, plugins will not work on iOS or macOS. For more info, see https://flutter.dev/platform-plugins To install: sudo gem install cocoapods [!] Android Studio (not installed) [!] IntelliJ IDEA Community Edition (version 2017.3.4) ✗ Flutter plugin not installed; this adds Flutter specific functionality. ✗ Dart plugin not installed; this adds Dart specific functionality. [!] Connected device ! No devices available ! Doctor found issues in 5 categories.
安装更新 Xcode => XCode ( with IOS Simulator )
安装 Android Studio => Android Studio ( AVD Manager -> Android Simulator )
安装 VS Code
各个IDE安装Flutter & Dart插件
# 列出模拟器> flutter emulator3 available emulators:apple_ios_simulator • iOS Simulator • Apple • iosPixel_API_28 • Pixel API 28 • Google • androidPixel_XL_API_30 • Pixel XL API 30 • Google • androidTo run an emulator, run 'flutter emulators --launch <emulator id>'.To create a new emulator, run 'flutter emulators --create [--name xyz]'.You can find more information on managing emulators at the links below: https://developer.android.com/studio/run/managing-avds https://developer.android.com/studio/command-line/avdmanager> flutter emulator --launch
Xcode -> Open Developer Tool -> Simulator
或者 cmd: open -a Simulator
> open -a Simulator > flutter create first_app > cd first_app > flutter run
Android Studio -> Tools -> AVD Manager -> Create Virtual Device
Sample: main.dart
import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Text( "Hello World", textDirection: textDirection.ltr, style: TextStyle(fontSize: 30, color: Colors.orange), ), ); }}
运行打包
flutter run --target=src/app.dartflutter build apk --target=src/app.dart
pubspec.yaml:
dependencies: flutter: sdk: flutter dio: ^3.0.10
> cd first_app> flutter pub get
https://zhuanlan.zhihu.com/p/151964543
flutter生命周期https://www.jianshu.com/p/7e8dff26f81a
/// for example: StatelessWidgetclass ProductItemWidget extends StatelessWidget { final String title; final String subTitle; final String imageUrl; const ProductItemWidget(this.title, this.subTitle, this.imageUrl); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), decoration: BoxDecoration(border: Border.all(width: 2)), child: Column( children: [ Text( title, style: TextStyle(fontSize: 20), ), Text(subTitle, style: TextStyle(fontSize: 16)), SizedBox( height: 10, ), Image.network(imageUrl), ], )); }}
import 'package:flutter/material.dart';/// for test: StatefulWidget 生命周期/*第一次初始化flutter: StatefulWidgetLcDemo constructor()flutter: StatefulWidgetLcDemo createState()flutter: _StatefulWidgetLcDemoState constructor()flutter: _StatefulWidgetLcDemoState initState()flutter: _StatefulWidgetLcDemoState didChangeDependencies()flutter: _StatefulWidgetLcDemoState build(ctx)保存-热加载更新flutter: StatefulWidgetLcDemo constructor()flutter: _StatefulWidgetLcDemoState didUpdateWidget(oldWidget)flutter: _StatefulWidgetLcDemoState build(ctx)press-setStateflutter: _StatefulWidgetLcDemoState setState() beforeflutter: _StatefulWidgetLcDemoState setState() doflutter: _StatefulWidgetLcDemoState setState() afterflutter: _StatefulWidgetLcDemoState build(ctx)*/class StatefulWidgetLcDemo extends StatefulWidget { StatefulWidgetLcDemo() { print("StatefulWidgetLcDemo constructor()"); } @override _StatefulWidgetLcDemoState createState() { print("StatefulWidgetLcDemo createState()"); return _StatefulWidgetLcDemoState(); }}class _StatefulWidgetLcDemoState extends State<StatefulWidgetLcDemo> { int count = 0; _StatefulWidgetLcDemoState() { print("_StatefulWidgetLcDemoState constructor()"); } @override void initState() { super.initState(); print("_StatefulWidgetLcDemoState initState()"); } @override void didChangeDependencies() { super.didChangeDependencies(); print("_StatefulWidgetLcDemoState didChangeDependencies()"); } @override void didUpdateWidget(StatefulWidgetLcDemo oldWidget) { super.didUpdateWidget(oldWidget); print("_StatefulWidgetLcDemoState didUpdateWidget(oldWidget)"); } @override Widget build(BuildContext context) { print("_StatefulWidgetLcDemoState build(ctx)"); return Container( child: RaisedButton( child: Text("Test StatefulWidget Lifecycle - Count: $count"), onPressed: () { print("_StatefulWidgetLcDemoState setState() before"); setState(() { count++; print("_StatefulWidgetLcDemoState setState() do"); }); print("_StatefulWidgetLcDemoState setState() after"); }, ), ); }}
App页面框架
容器:包含单个Widget布局:包含多个Widget
万能容器 Container
Sample: GridView
_buildContent(context,value){ return Container( height: StationTheme.stationHeight, child: GridView.builder( padding: EdgeInsets.all(8), scrollDirection: Axis.horizontal, itemCount: value.stationItems.length, itemBuilder: (context,index){ return InkWell( child:_buildStation(value.stationItems[index]), onTap: ()=>Navigator.of(context) .pushNamed(GlobalRoutes.Route_Detail,arguments: {'item':value.stationItems[index]}), ); }, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio:0.6 ), ) );}
Flutter手势系统分为两个层级
GestureDetector
来监听手势事件GestureDetector
通过检查出不为null的手势事件处理器来获悉需要识别的手势GestureDetector
手势检测,有7种类型事件:
InkWell
具有水波纹效果(溅墨效果)的点击事件控件注:当InkWell的子控件设置了背景色,是看不到溅墨效果的,需进行特殊处理(用Material & Ink 包裹)用InkWell包裹Image时,也是看不到溅墨效果的,建议使用 Ink.Image控件
静态化本地资源 assets
,fonts
pubspec.yaml:
dependencies: flutter: sdk: flutter flutter_svg: ^0.19.1 # svg flutter_staggered_grid_view: "^0.3.2" # 瀑布流插件flutter: assets: - assets/images/ - assets/icons/ fonts: - family: Nunito fonts: - asset: assets/fonts/Nunito/Nunito-Regular.ttf - asset: assets/fonts/Nunito/Nunito-SemiBold.ttf weight: 600 - asset: assets/fonts/Nunito/Nunito-Bold.ttf weight: 700
Overlay
组件管理多个页面,位于最上层的页面可见Overlay
里页面顺序来切换页面Page
也称为Screen
或Route
WidgetsApp
和MaterialApp
里会自动创建一个导航器,组件里可使用Navigator.of(context)
来获取其祖先组件里的导航器进入新页和返回
Navigator.push
方法进入某个页面(传递一个MaterialPageRoute对象)Navigator.pop
方法返回上一页Scaffold
,则导航条上会自动出现返回按钮跨页面传递数据
Navigator.push
返回Future
对象,会在Navigator.pop(value)
退出页面时完成,Future
得到pop
的值Navigator.pushNamed
进入指定名字的路由导航器嵌套
WillPopScope
来使内层导航器响应Android实体返回键 initialRoute:'/', routes:{ '/': (context) => NavigatorDemo(), '/home': (context) => Home(), '/about': (context) => MyPage(title: "About"), }
onPressed: () { // Navigator.of(context).push( // MaterialPageRoute( // builder: (BuildContext context) => MyPage(title: 'About')) //); Navigator.pushNamed(context, '/about'); },
onPressed: () { Navigator.pop(context); },
静态路由&动态路由 https://www.jianshu.com/p/66fa8c85cdc6
pubspec.yaml
dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter # 动态路由 fluro: 1.7.7
routes.dart:
import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:third_app/components/main_body.dart'; import 'package:third_app/pages/detail/detail.dart'; /* https://dart-pub.mirrors.sjtug.sjtu.edu.cn/packages/fluro https://zhuanlan.zhihu.com/p/107383867 https://segmentfault.com/a/1190000021488577 */ Handler homePageHandler = Handler( handlerFunc: (context, params) => MyMainBodyPageItem(Colors.grey,title: Text("Home Page"),), ); Handler detailPageHandler = Handler( handlerFunc: (context, params){ String detailId = params['id'].first; Map args = context.settings.arguments; debugPrint("detailPageHandler get detailId:$detailId,args:$args"); return DetailPage(detailId: detailId,args: args); }, ); Handler notFoundHandler = Handler( handlerFunc: (context, parameters){ return Container( alignment: Alignment.center, child: Text("出错啦。。。"), ); }, ); class Routes{ static String homePage = '/home'; static String detailPage = '/details/:id'; static void configRoutes(FluroRouter router) { router..define(homePage,handler:homePageHandler) ..define(detailPage, handler: detailPageHandler) ..notFoundHandler=notFoundHandler; } }
global.dart
import 'package:fluro/fluro.dart'; class Global{ static FluroRouter router; }
main.dart
void main() { final router = FluroRouter(); Routes.configRoutes(router); Global.router=router; runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Hi Dear', home: MyFrame(), // routes: onGenerateRoute: Global.router.generator, ); } }
Page A -> Page B
Navigator.pushNamed( context, Routes.detailPage, arguments: {'title':items[index]['title']} ).then((value)=>debugPrint("Get returnData: $value")); Global.router.navigateTo( context, items[index]['path'], transition: TransitionType.material, routeSettings: RouteSettings( arguments:{'title':items[index]['title']} ), ).then((value) => debugPrint("Get returnData: $value"));
Page B return to Page A
onPressed: () { // Global.router.pop(context, args); Navigator.pop(context,args); },
pubspec.yaml
dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter # 动态路由 auto_route: ^0.6.9 dev_dependencies: flutter_test: sdk: flutter # 路由生成 auto_route_generator: ^0.6.9 build_runner:
auto_routes/auto_routes.dart
import 'package:auto_route/auto_route_annotations.dart'; import 'package:third_app/auto_routes/auth_grard.dart'; import 'package:third_app/pages/detail/detail.dart'; import 'package:third_app/pages/frame/frame.dart'; import 'package:third_app/pages/home/home.dart'; @MaterialAutoRouter( routes: <AutoRoute>[ MaterialRoute(path: '/',page: MyFrame,initial: true), MaterialRoute(path:'/home',page: HomePage), MaterialRoute(path:'/details/:id',page: DetailPage,guards: [AuthGuard]), ], ) //CustomAutoRoute(..config) class $AppRouter { }
auto_routes/auth_guard.dart
import 'package:auto_route/auto_route.dart'; class AuthGuard extends RouteGuard{ @override Future<bool> canNavigate(ExtendedNavigatorState<RouterBase> navigator, String routeName, Object arguments) async { print("This is AuthGuard canNavigate: $routeName,$arguments"); return true; } }
cmd to generate auto_routes/auto_routes.gr.dart
flutter packages pub run build_runner build
main.dart
void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Hi Dear', home: MyFrame(), // 注册路由 (auto_flutter) - Without ExtendedNavigator will lose support for RouteGuards and auto-nested navigation handling // onGenerateRoute: AppRouter(), // 注册路由 (auto_flutter) builder: ExtendedNavigator<AppRouter>( // initialRoute: Routes.myFrame, router: AppRouter(), guards:[AuthGuard()] ), ); } }
Page A -> Page B
ExtendedNavigator.of(context).push( items[index]['path'], arguments:DetailPageArguments(detailId:'$index',args:{'title':items[index]['title']} ), ).then((value) => debugPrint("Get returnData: $value"));
Page B return to Page A
ExtendedNavigator.of(context).pop(args);
AppBar
return Scaffold( backgroundColor: Colors.grey[300], appBar: AppBar( leading: IconButton( icon:Icon(Icons.menu), tooltip: 'Navigation', onPressed: ()=>debugPrint('Navigation button is pressed.'), ), actions: [ IconButton( icon:Icon(Icons.search), tooltip: 'Search', onPressed: ()=>debugPrint('Search button is pressed.'), ) ], title: Text("Second App") ), body: null );
import 'package:flutter/material.dart';class BottomNavigationBarDemo extends StatefulWidget { BottomNavigationBarDemo({Key key}) : super(key: key); @override _BottomNavigationBarDemoState createState() => _BottomNavigationBarDemoState();}class _BottomNavigationBarDemoState extends State<BottomNavigationBarDemo> { int _currentIndex = 0; void _onTapHandler(int index) { setState(()=>_currentIndex=index); } @override Widget build(BuildContext context) { return BottomNavigationBar( // 底部导航栏 type: BottomNavigationBarType.fixed, // item大于等于4个时需设置 fixedColor: Colors.black, currentIndex: _currentIndex, onTap: _onTapHandler, items: [ BottomNavigationBarItem(icon: Icon(Icons.explore), title: Text("Explore")), BottomNavigationBarItem(icon: Icon(Icons.history), title: Text("History")), BottomNavigationBarItem(icon: Icon(Icons.list), title: Text("List")), BottomNavigationBarItem(icon: Icon(Icons.person), title: Text("My")), ]); }}
Example 1 :
return BottomAppBar( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton(icon: Icon(Icons.home),onPressed: null,), IconButton(icon: Icon(Icons.list),onPressed: null,), IconButton(icon: Icon(Icons.person),onPressed: null,), ], ) );
Example 2 :
return BottomAppBar( child: Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children:[ Expanded(child:IconButton(icon: Icon(Icons.home),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.list_alt_rounded),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.person),onPressed: null,)), Expanded(child:IconButton(icon: Icon(Icons.ac_unit),onPressed: null,)), ] ), ), );
Example 3:
return BottomAppBar( child: Container( padding: EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.end, children: [ InkWell( onTap: ()=>_onTapHandler(0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.home,color: _currentIndex==0?Colors.red:Colors.black,), Text("首页",style: TextStyle(color: _currentIndex==0?Colors.red:Colors.black),) ], ), ), InkWell( onTap: ()=>_onTapHandler(1), child:Container( child:Text("Do",style: TextStyle(color:Colors.white),), padding: EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(35), color: _currentIndex==1?Colors.red:Colors.grey, ), ) ), InkWell( onTap: ()=>_onTapHandler(2), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person,color: _currentIndex==2?Colors.red:Colors.black,), Text("我的",style: TextStyle(color: _currentIndex==2?Colors.red:Colors.black),) ], ), ), ], ), ) );
https://juejin.cn/post/6844903660816695309
Flutter BottomNavigationBar切换页面被重置问题(保存状态)https://www.jianshu.com/p/87e545b889cd
Flutter底部tab切换保持页面状态的2种方法(转)https://www.jianshu.com/p/b7dd54f0bcbd?utm_campaign=harukihttps://www.jianshu.com/p/369f00a40cc2
切换后页面状态的保持 AutomaticKeepAliveClientMixin
https://blog.csdn.net/niceyoo/article/details/92855534https://blog.csdn.net/u010842313/article/details/105554390
Flutter实现页面切换后保持原页面状态的3种方法https://www.jb51.net/article/157680.htmhttps://my.oschina.net/u/4581368/blog/4372254
AutomaticKeepAliveClientMixin
Sample: IndexedStack
class IndexBody extends StatelessWidget { final int currentIndex; final List<Widget> items = [ HomePage(), DoPage(), MyPage(), ]; IndexBody({Key key,this.currentIndex=0}) : super(key: key); @override Widget build(BuildContext context) { debugPrint("build IndexBody"); return IndexedStack( index: currentIndex, children: items, ); }}
Sample: AutomaticKeepAliveClientMixin
class ProjectAnnounceView extends StatefulWidget { ProjectAnnounceView({Key key}) : super(key: key); @override _ProjectAnnounceViewState createState() => _ProjectAnnounceViewState();}class _ProjectAnnounceViewState extends State<ProjectAnnounceView> with AutomaticKeepAliveClientMixin{ @override // TODO: implement wantKeepAlive bool get wantKeepAlive => true; @override Widget build(BuildContext context) { return _buildAnnouncementList(getDataOfAnnouncements()); } Widget _buildAnnouncementList(announcementList){ return Container( margin: EdgeInsets.only(top:10), child: ListView.builder( itemCount: announcementList.length+1, itemBuilder: (_,index){ if(index==announcementList.length) return Container( margin: EdgeInsets.all(20), alignment: Alignment.center, child: Text("—— End ——",style: TextStyle(color: Colors.grey),), ); return _buildItem(announcementList[index]); }, ) ); }}
TabBar & TabView & TabControllerhttps://www.cnblogs.com/joe235/p/11213861.html
Flutter TabBar、TabBarView、 TabController 实现 Tab 标签菜单布局http://www.ptbird.cn/flutter-tab-tabcontroller.html
TabController
DefaultTabController
TabController
实现SingleTickerProviderStateMixin
class Home extends StatelessWidget { const Home({Key key}) : super(key: key); @override Widget build(BuildContext context) { // TabController & TabBar(tabs) & TabBarView return DefaultTabController( length: 5, child: Scaffold( backgroundColor: Colors.grey[300], floatingActionButton: FloatingActionButton( child: Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); }, ), appBar: AppBar( elevation: 0, actions: [ IconButton( icon: Icon(Icons.search), tooltip: 'Search', onPressed: () => debugPrint('Search button is pressed.'), ) ], bottom: TabBar( tabs: [ Tab(icon: Icon(Icons.local_florist)), Tab(icon: Icon(Icons.change_history)), Tab(icon: Icon(Icons.directions_bike)), Tab(icon: Icon(Icons.view_quilt)), Tab(icon: Icon(Icons.view_agenda)), ], unselectedLabelColor: Colors.black38, indicatorColor: Colors.black54, indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 1.0, ), title: Text("Second App")), body: TabBarView( children: [ Icon(Icons.local_florist, size: 128, color: Colors.black12), Icon(Icons.change_history, size: 128, color: Colors.black12), Icon(Icons.directions_bike, size: 128, color: Colors.black12), Icon(Icons.view_quilt, size: 128, color: Colors.black12), Icon(Icons.view_agenda, size: 128, color: Colors.black12), ], ), bottomNavigationBar: BottomNavigationBarDemo(), )); }}
class ChannelFmIndex extends StatefulWidget { ChannelFmIndex({Key key}) : super(key: key); @override _ChannelFmIndexState createState() => _ChannelFmIndexState();}class _ChannelFmIndexState extends State<ChannelFmIndex> with SingleTickerProviderStateMixin { TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // 创建 TabController } @override Widget build(BuildContext context) { return Scaffold( // appBar: _buildAppBar(context), appBar: _buildHeader(), body: _buildTabView(context), ); } _buildHeader(){ return AppBar( elevation: 0, automaticallyImplyLeading: false, titleSpacing: 0, toolbarHeight: 160, title: _buildHeadCard(), bottom: PreferredSize( preferredSize: Size.fromHeight(50), child: _buildHeadTabBar(), ), ); } _buildHeadTabBar(){ return Container( decoration: BoxDecoration( color: Colors.white, border: Border.symmetric( horizontal: BorderSide(color: Colors.grey[50],width: 8) ) ), child: TabBar( controller: _tabController, // 配置 TabController indicatorColor: Colors.black87, indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 1.0, tabs: [ Tab(child: Text("最新"),), Tab(child: Text("动态"),), Tab(child: Text("简介"),), ], ), ); } _buildTabView(BuildContext context){ return TabBarView( controller: _tabController, // 配置 TabController children: <Widget>[ _buildTabOfLatest(context), _buildTabOfActivity(context), _buildTabOfIntro() ], ); } _buildTabOfLatest(BuildContext context){ // ... } _buildTabOfActivity(context){ // ... } _buildTabOfIntro(){ // ... }}
第三方组件: flutter_swiper
https://www.jianshu.com/p/9bdfc5a00877https://zhuanlan.zhihu.com/p/88790923
pubspec.yaml
dependencies: flutter: sdk: flutter # 轮播图 flutter_swiper: ^1.1.6
home_banner.dart
import 'package:flutter/material.dart'; import 'package:flutter_swiper/flutter_swiper.dart'; /* Ref Doc: https://www.cnblogs.com/joe235/p/11251710.html https://zhuanlan.zhihu.com/p/88790923 https://segmentfault.com/a/1190000021488577 */ class HomeBanner extends StatelessWidget { final List items; HomeBanner({Key key, this.items}) : assert(items != null && items.length != 0), super(key: key); @override Widget build(BuildContext context) { return Container( height: 200, child: Swiper( itemCount: items.length, itemBuilder: (context, index) { // return InkWell( // child: Image.network(items[index]['image'], fit: BoxFit.fill), // onTap: (){ // debugPrint('banner click to path: ${items[index]['path']}'); // }, // ); return Image.network(items[index]['image'], fit: BoxFit.fill); }, pagination: new SwiperPagination( // builder: SwiperPagination.fraction // builder: RectSwiperPaginationBuilder( // color: Colors.black, // size: Size(20,10) // ) ), autoplay: true, autoplayDelay:3000, onTap: (index){ debugPrint("banner onTap:$index ${items[index]['path']}"); } // viewportFraction: 0.8, // scale: 0.9, ), ); } }
home.dart
Widget _buildHomeBanner(){ List items=[ { 'image':'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1603365312,3218205429&fm=26&gp=0.jpg', 'path':'/details/a', 'title':'AAA' }, { 'image':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2005235653,1742582269&fm=26&gp=0.jpg', 'path':'/details/b', 'title':'BBB' }, { 'image':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1355153719,3297569375&fm=26&gp=0.jpg', 'path':'/details/c', 'title':'CCC' }, ]; return HomeBanner(items: items); }
RefreshIndicator
下拉刷新https://www.cnblogs.com/darecy/archive/2020/05/10/12863080.html
_buildRefreshIndicator(String name,Widget child){ return RefreshIndicator( onRefresh: () async { debugPrint("+++ $name onRefresh! +++"); }, child: child );}
flutter_easyrefresh
第三方组件
下拉刷新以及上拉加载
pubspec.yaml
dependencies: flutter: sdk: flutter # 下拉刷新以及上拉加载 flutter_easyrefresh: ^2.1.6
home.dart
Widget _buildHomeBody(){ /* https://github.com/xuelongqy/flutter_easyrefresh/blob/v2/README.md */ return EasyRefresh( child: _buildHomeBodyList(), header: ClassicalHeader( refreshText:'下拉刷新', refreshReadyText:'准备刷新', refreshingText:'刷新获取中', refreshedText:'刷新完成', refreshFailedText: '刷新失败', infoText: '更新时间 %T', completeDuration: Duration(milliseconds: 300), textColor: Colors.black54, infoColor: Colors.black54, showInfo:false ), footer: ClassicalFooter( loadText:'上滑加载', loadReadyText: '加载中', loadingText:'加载中', loadedText:'加载完成', loadFailedText:'加载失败', noMoreText: '我也是有底线的', infoText: '更新时间 %T', textColor: Colors.black54, showInfo: false ), onRefresh: () async{ debugPrint("HomePage onRefresh"); }, onLoad: () async{ debugPrint("HomePage onLoad"); }, ); } Widget _buildHomeBodyList(){ return ListView( children: [ _buildHomeBanner(), ], ); }
Flutter 滚动控件篇-->滚动监听及控制(ScrollController)https://www.mk2048.com/blog/blog_h1ck21icak0hj.html
Flutter 滚动监听及实战appBar滚动渐变https://www.jianshu.com/p/b0b1c6308674
offset
:可滚动组件当前的滚动位置。jumpTo(double offset)
跳转到指定位置,offset 为滚动偏移量。animateTo(double offset,@required Duration duration,@required Curve curve)
同 jumpTo(double offset)
一样,不同的是 animateTo
跳转时会执行一个动画,需要传入执行动画需要的时间和动画曲线。List<ScrollPosition>
数组controller.positions.elementAt(0).pixels
animateTo()
和 jumpTo()
,他们才是真正控制跳转到滚动位置的方法,在 ScrollController 中这两个同名方法,内部最终都会调用 ScrollPosition 这两个方法ScrollController方法createScrollPosition
void attach(ScrollPosition position)
方法来将创建好的 ScrollPosition 信息添加到 positions 属性中,这一步称为“注册位置”,只有注册后animateTo()
和 jumpTo()
才可以被调用。void detach(ScrollPosition position)
方法,将其 ScrollPosition 对象从 ScrollController 的positions 属性中移除,这一步称为“注销位置”,注销后 animateTo() 和 jumpTo() 将不能再被调用。在接收到滚动事件时,参数类型为ScrollNotification
metrics
属性,类型是ScrollMetrics
,该属性包含当前ViewPort
及滚动位置等信息:switch (notification.runtimeType){case ScrollStartNotification: print("开始滚动"); break;case ScrollUpdateNotification: print("正在滚动"); break;case ScrollEndNotification: print("滚动停止"); break;case OverscrollNotification: print("滚动到边界"); break;}
onNotification
回调为通知处理回调,他的返回值时布尔类型(bool),当返回值为 true 时,阻止冒泡,其父级 Widget 将再也收不到该通知;当返回值为 false 时继续向上冒泡通知。https://zhuanlan.zhihu.com/p/84716922
确定可滚动控件的物理特性
Sample:
CustomScrollView( // ... physics: const BouncingScrollPhysics());ListView.builder( // ... physics: const AlwaysScrollableScrollPhysics());GridView.count( // ... physics: NeverScrollableScrollPhysics());
应用:两个滚动列表嵌套,设置NeverScrollableScrollPhysics让内部滚动失效,依赖外部的滚动
_buildItems(value){ return ListView.builder( itemCount: value.activityItems.length+1, shrinkWrap: true, // 父视图的大小跟随子组件的内容大小 for solve: Error: Flutter Horizontal viewport was given unbounded height.width. (或固定父容器高度. 或用Expanded/Flexible包裹,把剩余空间全部占掉) physics: new NeverScrollableScrollPhysics(), itemBuilder: (ctx,index){ if(index==value.activityItems.length){ return _buildEndInfo(value); } return _buildActivity(value.activityItems[index]); } ); }
ScrollController _scrollController = new ScrollController();@overridevoid initState() { debugPrint("-- HomeMovie initState --"); // TODO: implement initState super.initState(); _scrollController.addListener(() { // debugPrint("_scrollController:${_scrollController.position.pixels}"); if(_scrollController.position.pixels==_scrollController.position.maxScrollExtent){ debugPrint("-- HomeMovie Trigger load --"); widget.homeMovieState.load(); } });}@overridevoid dispose() { debugPrint("-- HomeMovie dispose --"); _scrollController.dispose(); // TODO: implement dispose super.dispose();}/// ListView.builder( controller: _scrollController, itemBuilder: (ctx,index){...},...);
Flutter 滚动距离来设置TabBar的位置,点击TabBar滚动的到指定的位置https://blog.csdn.net/yujunlong3919/article/details/105107195
flutter滚动到列表指定元素https://chentaoqian.com/?p=613
scrollable_positioned_list
Scrollable.ensureVisible(context)
,该方式不仅能达到效果,还适用于各种尺寸的widget。但这方案也有个问题,就是不能跳转到不可见的组件(non-visible)Sample:
GlobalKey()
,并在外部存储该key。Scrollable.ensureVisible(context)
,传入的context是上一步key的currentContext。class ScrollView extends StatelessWidget { final dataKey = new GlobalKey(); @override Widget build(BuildContext context) { return new Scaffold( primary: true, appBar: new AppBar( title: const Text('Home'), ), body: new SingleChildScrollView( child: new Column( children: [ new SizedBox(height: 160.0, width: double.infinity, child: new Card()), new SizedBox(height: 160.0, width: double.infinity, child: new Card()), new SizedBox(height: 160.0, width: double.infinity, child: new Card()), // destination new Card( key: dataKey, child: new Text("data\n\n\n\n\n\ndata"), ) ], ), ), bottomNavigationBar: new RaisedButton( onPressed: () => Scrollable.ensureVisible(dataKey.currentContext), child: new Text("Scroll to data"), ), ); }}
https://www.jianshu.com/p/cf8e92f76bdbhttps://blog.csdn.net/yechaoa/article/details/90701321
_buildSliverBody(context) { return CustomScrollView( slivers: [ _buildSliverAppBar(), _buildSliverPanding(), _buildSliverFillRemaining(), _buildSliverFillViewport(), _buildSliverGrid(), _buildSliverList(), ], ); }
_buildSliverAppBar() { return SliverAppBar( title: Text("标题"), expandedHeight: 180.0, floating: false, pinned: true, snap: false, flexibleSpace: new FlexibleSpaceBar( title: new Text("标题标题标题"), centerTitle: true, collapseMode: CollapseMode.pin, ), onStretchTrigger:()async{ print("onStretchTrigger");return; } ); return SliverAppBar( floating: false, pinned: true, expandedHeight: 180, flexibleSpace: FlexibleSpaceBar( title: Text( "Silver Demo", style: TextStyle(letterSpacing: 3, fontWeight: FontWeight.w400), ), background: Image.network( "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1549239440,280119214&fm=26&gp=0.jpg", fit: BoxFit.cover), ), ); }
_buildSliverList() { return SliverFixedExtentList( itemExtent: 40.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Card( child: Container( alignment: Alignment.center, color: Colors.primaries[(index % 18)], child: Text(''), ), ); }, ), ); }
_buildSliverGrid() { return SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 2.0, mainAxisSpacing: 10, crossAxisSpacing: 10, ), ///子Item构建器 delegate: new SliverChildBuilderDelegate( (BuildContext context, num index) { ///每一个子Item的样式 return Container( color: Colors.blue, child: Text("grid $index"), ); }, ///子Item的个数 childCount: 30, ), ); }
在CustomScrollView 中是只能使用Sliver系的控件,如果在CustomScrollView 中想要嵌套其他非 Sliver 系就必须要使用SliverToBoxAdapter包装一下
_buildSliverPanding() { return SliverPadding( padding: EdgeInsets.all(8), sliver: SliverToBoxAdapter( child: Container( height: 40, color: Colors.grey, child: Text("SliverPadding & SliverToBoxAdapter"), ), )); }
_buildSliverFillRemaining() { return SliverFillRemaining( child: Container( height: 40, color: Colors.lightBlue, child: Text("SliverFillRemaining"), ), ); }
_buildSliverFillViewport() { return SliverFillViewport( viewportFraction: 1.0, delegate: SliverChildBuilderDelegate( (_, index) => Container( margin: EdgeInsets.all(8), child: Text('Item $index'), alignment: Alignment.center, color: Colors.indigoAccent), childCount: 10)); }
https://segmentfault.com/a/1190000022575678
_buidBody() { return NestedScrollView( headerSliverBuilder: (_,innerBoxIsScrolled){ return [ SliverToBoxAdapter(child: _buildPlayPanel(),), SliverAppBar( automaticallyImplyLeading: false, elevation: 0, floating: true, pinned: true, title: _buildTabBar() ), ]; }, body: _buildTabView() ); }
Flutter 里 NestedScrollView body 嵌套滚动的问题https://www.v2ex.com/t/655844http://codingdict.com/questions/97543
在子组件里控制父级组件的滚动 => 但“会导致头部滚动折叠的很快”,不推荐!
_scrollController = ScrollController();_scrollController.addListener((){ var innerPos = _scrollController.position.pixels; var maxOuterPos = widget.parentController.position.maxScrollExtent; var currentOutPos = widget.parentController.position.pixels; if(innerPos >= 0 && currentOutPos < maxOuterPos) { //widget.parentController.position.jumpTo(innerPos+currentOutPos); widget.parentController.position.animateTo(innerPos+currentOuterPos, duration: Duration(seconds:1), curve: Curves.easeOut); }else{ var currenParentPos = innerPos + currentOutPos; widget.parentController.position.jumpTo(currenParentPos); }});widget.parentController.addListener((){ var currentOutPos = widget.parentController.position.pixels; if(currentOutPos <= 0) { _scrollController.position.jumpTo(0); }});
Flutter Exception降到万分之几的秘密https://zhuanlan.zhihu.com/p/53443293
https://blog.csdn.net/wangfeijn/article/details/90033008https://segmentfault.com/a/1190000022280728https://ducafecat.tech/2020/06/05/flutter-project/flutter-project-news-12-error-sentry/
捕捉异常
try/catch/finally
即可runZonedGuarded(...)
方法,指定一个代码执行的环境空间异常:
异常处理:
try/catch/finally
: catch sync dart error.runZonedGuarded
: catch uncatched dart error.FlutterError.onError = (FlutterErrorDetails details) async {};
: catch flutter error when working in release mode.ErrorWidget.builder=(FlutterErrorDetails details){};
: a widget show flutter error when working in debug mode.Sample:
main.dart:
import 'dart:async'; import 'package:five_demo/pages/app/app_page.dart'; import 'package:five_demo/utils/log_utils.dart'; import 'package:flutter/material.dart'; import 'global/global.dart'; // void main() { // runApp(MyApp()); // } void main() async { // catch uncatched dart error runZonedGuarded(() async { Global.init().then((value) { // runApp(Global.wrapGlobalProviders(MyApp())); runApp(AppPage()); }); }, (error,stack)=>LogUtils.error("$error \n $stack")); }
log_utils.dart:
import 'dart:async'; import 'package:flutter/material.dart'; class LogUtils{ static info(Object object){ print("=== INFO === : $object"); } static error(Object object){ print("=================== ERROR Begin"); print(object); print("=================== ERROR End"); } // 是否开发环境 static bool get isInDebugMode { bool isRelease = bool.fromEnvironment("dart.vm.product"); return !isRelease; } /// Note: debug mode doesn't work, work in release mode ! static setFlutterErrorHandlerOnRelease(){ FlutterError.onError = (FlutterErrorDetails details) async { print("=================== CAUGHT FLUTTER ERROR"); if (isInDebugMode == true) { FlutterError.dumpErrorToConsole(details); } else { Zone.current.handleUncaughtError(details.exception, details.stack); } }; } /// Note: only show in debug mode ,won't show in release mode ! static setFlutterErrorWidgetOnDebug(){ ErrorWidget.builder=(FlutterErrorDetails details){ print("=================== BUILD ERROR WIDGET"); print(details.toString()); return SingleChildScrollView( // decoration: BoxDecoration( // image: DecorationImage( // image: new AssetImage("assets/images/default/empty.png"), // ) // ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children:[ Text("Exception!",style: TextStyle(color: Colors.red,fontSize: 26),), // Image.network( // 'https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1671659209,570778771&fm=26&gp=0.jpg'), Text("${details.exception}",style: TextStyle(color:Colors.red,fontSize: 18),textDirection: TextDirection.ltr,) ] ), ); }; } }
global.dart
class Global{ static Future init() async { LogUtils.info("[Global] init() start"); WidgetsFlutterBinding.ensureInitialized(); LogUtils.info("[Global] set FlutterErrorHander"); LogUtils.setFlutterErrorWidgetOnDebug(); LogUtils.setFlutterErrorHandlerOnRelease(); // ... } }
flutter build ios --releaseflutter run --release
Xcode:
https://www.jianshu.com/p/69e1efc2fc55http://www.cocoachina.com/articles/475845https://www.codingsky.com/doc/flutter/ios-release-build.html
在 Identity 部分:
在 Signing 部分:
Flutter 基础组件:输入框和表单https://www.cnblogs.com/parzulpan/p/12066691.html
Flutter TextField详解https://blog.csdn.net/yechaoa/article/details/90906689
Flutter 实现一个登录界面http://www.cocoachina.com/articles/29529?filter=rec
Flutter监听TextField焦点事件,点击与清除焦点https://www.uedbox.com/post/65066/
Flutter Form、TextFormField及表单验证、表单输入框聚焦http://www.ptbird.cn/flutter-form-textformfield.html
Flutter底部弹出TextField评论输入框并且自适应高度https://www.jianshu.com/p/09ae2b6995e7?utm_campaign=haruki
flutter中关于软键盘弹起导致的问题https://www.jianshu.com/p/4dab8a87f28b
解决问题1中overflow提示的两种办法:
resizeToAvoidBottomInset
属性设置为false
,这样在键盘弹出时将不会resize原高度 - MediaQuery.of(context).viewInsets.bottom
,键盘弹出时布局将重建,而这个MediaQuery.of(context).viewInsets.bottom
变量在键盘弹出前是0,键盘弹起后的就是键盘的高度解决问题2的办法:
flutter TextField 输入框被软键盘挡住的解决方案https://www.cnblogs.com/tianmiaogongzuoshi/p/11181782.html
页面元素的最外层肯定得嵌套一层SingleChildScrollView
(SingleChildScrollView
元素内部不能和 Expanded 的flex 直接填充,会冲突)
body: Container( //SingleChildScrollView 的父级元素得有高度 最外层Container默认 填充全部 child: SingleChildScrollView( ........ )
flutter中如何监听键盘弹出关闭https://segmentfault.com/a/1190000022495736
Flutter showModalBottomSheet & Textfield 制作底部评论框并解决bughttps://juejin.cn/post/6844903846645334023
Flutter 弹出键盘认识https://juejin.cn/post/6844903749362647048
TextField, 全局点击空白处隐藏键盘https://my.oschina.net/u/4082303/blog/4543122?utm_source=osc_group_android为 TextField 添加 focusNode
,点击空白处时使 TextField 失去焦点
FocusScope.of(context).requestFocus(new FocusNode());
class DismissKeyboardDemo extends StatelessWidget { final FocusNode focusNode = FocusNode(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: GestureDetector( onTap: () { focusNode.unfocus(); }, child: Container( color: Colors.transparent, alignment: Alignment.center, child: TextField( focusNode: focusNode, ), ), ), ); }}
当 App 中有多个页面多个 TextField 时,此方式会增加大量重复的代码,因此全局添加点击空白处的监听:
onTap: () { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) { FocusManager.instance.primaryFocus.unfocus(); }},
也可以使用如下方式隐藏键盘:
SystemChannels.textInput.invokeMethod('TextInput.hide');
MVC、MVP、BloC、Redux四种架构在Flutter上的尝试https://www.jianshu.com/p/ba3414457419
Flutter State Management状态管理全面分析https://www.jianshu.com/p/9334b8f68004
StatefulWidget
-> setState
自身组件动态维护Provider
(InheritedWidget
)-> 与其他组件交互Bloc
(Stream
) -> 与多个监听组件同步信息https://www.jianshu.com/p/4d5e712594b4https://www.jianshu.com/p/e31e8268d2cd
pubspec.yaml
dependencies: # bloc flutter_bloc: ^6.1.1
counter_page.dart
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:third_app/pages/counter/counter_event.dart'; import 'counter_bloc.dart'; class CounterPage extends StatelessWidget { CounterPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Container( child: BlocProvider<CounterBloc>( create: (context) => CounterBloc(0), // child: BlocBuilder<CounterBloc, int>( // builder: (context, state) => // _buildContent(context.watch<CounterBloc>(), state), // ) child: BlocListener<CounterBloc,int>( listener:(context,state)=>print("state:$state"), child:BlocBuilder<CounterBloc, int>( builder: (context, state) => _buildContent(context.watch<CounterBloc>(),state), ) ), ) ); } _buildContent(CounterBloc bloc, int state) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text("Count:$state"), OutlineButton( onPressed: () => bloc.add(CounterEvent.increment), child: Text("Add")), OutlineButton( onPressed: () => bloc.add(CounterEvent.decrement), child: Text("Sub")), ], ); } }
counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:third_app/pages/counter/counter_event.dart'; class CounterBloc extends Bloc<CounterEvent,int>{ CounterBloc(int initialState) : super(initialState); @override Stream<int> mapEventToState(CounterEvent event) async * { switch(event){ case CounterEvent.decrement: yield state-1; break; case CounterEvent.increment: yield state+1; break; default: throw Exception('oops'); } } }
counter_event.dart
enum CounterEvent { increment, decrement }
Provider组件
pubspec.yaml
dependencies: # 状态管理 provider: ^4.3.2+2
https://www.jianshu.com/p/a87ebd2d3296http://www.cainiaoxueyuan.com/xcx/13599.htmlhttps://www.cnblogs.com/crazycode2/p/11407967.htmlhttps://www.jianshu.com/p/bf2f33b2b5efhttps://cloud.tencent.com/developer/article/1485323
Provider.value
/ Provider.create
=> 推荐create(在销毁时自动调用ChangeNotifier
中的dispose()
方法释放一些资源)context.read
/ context.watch
Provider.of<T>(this, listen: false);
)provider.of(context)
所在子widget不管是否是const都被重建后刷新数据 => 将会把调用了该方法的context
作为听众,并在 notifyListeners
的时候通知其刷新。Consumer
,Select
包裹,监听到T改变,会重新构建 => 极大地缩小你的控件刷新范围(可使用Selector
达到更精细地控制,eg: 在list的长度发生改变时才会重新渲染,其内部元素改变时并不会触发重绘)(BuildContext context, T model, Widget child)
@override Widget build(BuildContext context) { return builder( context, Provider.of<T>(context), child, ); }
Provider
最基础的provider,它会获取一个值并将它暴露出来ListenableProvider
用来暴露可监听的对象,该provider将会监听对象的改变以便及时更新组件状态ChangeNotifierProvider
ListerableProvider
依托于ChangeNotifier的一个实现,它将会在需要的时候自动调用ChangeNotifier.dispose方法ValueListenableProvider
监听一个可被监听的值,并且只暴露ValueListenable.value方法StreamProvider
监听一个流,并且暴露出其最近发送的值FutureProvider
接受一个Future作为参数,在这个Future完成的时候更新依赖ProxyProvider
能够将不同provider中的多个值整合成一个对象,并将其发送给外层provider,当所依赖的多个provider中的任意一个发生变化时,这个新的对象都会更新使用目的:
当没有BuildContext时可以使用Consumer
@override // ERROR:ProviderNotFoundError 因为该context中并没有Provider Widget build(BuildContext context) { return ChangeNotifierProvider( builder: (_) => Foo(), child: Text(Provider.of<Foo>(context).value), ); } @override // OK Widget build(BuildContext context) { return ChangeNotifierProvider( builder: (_) => Foo(), child: Consumer<Foo>( builder: (_, foo, __) => Text(foo.value), }, ); }
它通过更细粒度的重构来帮助性能优化。
class RedBox extends StatelessWidget { @override Widget build(BuildContext context) { print("---------RedBox---------build---------"); return Container( color: Colors.red, width: 150, height: 150, alignment: Alignment.center, child: Consumer<CountState>(builder: (ctx,state,child){ print("---------RedBox----Consumer-----build---------"); return Text("Red:${state.count}", style: TextStyle(fontSize: 20),); }), ); } }
Consumer 源码
class Consumer<T> extends SingleChildStatelessWidget { /// {@template provider.consumer.constructor} /// Consumes a [Provider<T>] /// {@endtemplate} Consumer({ Key key, @required this.builder, Widget child, }) : assert(builder != null), super(key: key, child: child); /// {@template provider.consumer.builder} /// Build a widget tree based on the value from a [Provider<T>]. /// /// Must not be `null`. /// {@endtemplate} final Widget Function(BuildContext context, T value, Widget child) builder; @override Widget buildWithChild(BuildContext context, Widget child) { return builder( context, Provider.of<T>(context), child, ); }}
注:传入的context是谁的BuildContext? 每个Widget都有属于自己的元素Element,在该Element进行mount的时候回将自身化作美丽的天使(Context)传入组件或State的build方法中来供你使用
https://book.flutterchina.club/chapter7/futurebuilder_and_streambuilder.html
FutureBuilder({ this.future, this.initialData, @required this.builder, // Function (BuildContext context, AsyncSnapshot snapshot) snapshot 包含当前异步任务的状态信息及结果信息})
StreamBuilder({ Key key, this.initialData, Stream<T> stream, @required this.builder,})
snapshot.hasError
判断异步任务是否有错误snapshot.connectionState
获取异步任务的状态信息
enum ConnectionState { /// 当前没有异步任务,比如[FutureBuilder]的[future]为null时 none, /// 异步任务处于等待状态 waiting, /// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。 active, /// 异步任务已经终止. done,}
Flutter 实践 MVVMhttps://cloud.tencent.com/developer/article/1372224
https://blog.csdn.net/baidu_34120295/article/details/86495861
1.首先先需要对控件进行渲染
初始化GlobalKey :GlobalKey anchorKey = GlobalKey();
2.在需要测量的控件的下面添加key:
child: Text("点击弹出悬浮窗", style: TextStyle(fontSize: 20), key: anchorKey),
3.获取控件的坐标:
RenderBox renderBox = anchorKey.currentContext.findRenderObject();var offset = renderBox.localToGlobal(Offset.zero);
控件的横坐标:offset.dx
控件的纵坐标:offset.dy
如果想获得控件正下方的坐标:
RenderBox renderBox = anchorKey.currentContext.findRenderObject();var offset = renderBox.localToGlobal(Offset(0.0, renderBox.size.height));
控件下方的横坐标:offset.dx
控件下方的纵坐标:offset.dy
https://www.cnblogs.com/pjl43/p/9615685.html
通常情况下,显示有四种情况:
1.(visible)显示2.(invisible)隐藏:这种隐藏是指在屏幕中占据空间,只是没有显示。这种情况出现场景如:用带有背景色的Container Widget去包含一个不可见的Image,当从网络中加载图片后才让它显示,这是为了避免图片显示后让页面布局改变发生跳动。3.(Offscreen)超出屏幕,同样占据空间4.(Gone)消失:这种是指Widget没有被rendered,不存在于wedget tree中
https://blog.csdn.net/u012109585/article/details/108127209
Flutter之自定义AppBar并实现滑动渐变https://blog.csdn.net/u013600907/article/details/101456290https://www.jianshu.com/p/6fe2e74d35bf
用到了NotificationListener这个widget,借助这个widget可以监听滚动的高度。appBar则使用自定义widget实现,给外层嵌套一个opacity组件,通过滚动监听高度变化然后改变appBar透明度即可。
scrollNotification.depth 的值 0 表示其子组件(只监听子组件,不监听孙组件);scrollNotification is ScrollUpdateNotification 来判断组件是否已更新,ScrollUpdateNotification 是 notifications 的生命周期一种情况,分别有一下几种:
NotificationListener( onNotification: (scrollNotification) { if (scrollNotification is ScrollUpdateNotification && scrollNotification.depth==0) { _onScroll(scrollNotification.metrics.pixels); }}
判断条件里的ScrollUpdateNotification是指widget组件位置发生改变才会执行相应的逻辑
_onScroll (offset) { double alpha = offset / APPBAE_SCROLL_OFFSET; // APPBAE_SCROLL_OFFSET为appBar高度 if (alpha < 0) { // alpha = 0; return ; } else if (alpha > 1) { // alpha = 1; return ; } setState(() { alphaAppBar = alpha; }); }
ScrollController / NotificationListenerhttps://www.jianshu.com/p/b0b1c6308674
=> 内容如果过多的话,你这种写法会造成页面的严重卡顿?
通过LayoutBuilder组件可以获取父组件的约束尺寸eg: 根据组件的大小确认组件的外观,比如竖屏的时候上下展示,横屏的时候左右展示,
Flutter如何检查Sliver AppBar是否展开或折叠?https://www.javaroad.cn/questions/81420
_buildSliverToBoxAdapter(){ return SliverToBoxAdapter( child:Container( height: 30, // 当设置父组件的宽高大于100时显示蓝色,小于100时显示红色。 child: _buildLayoutBuilder() ), ); }
_buildLayoutBuilder(){ return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var color = Colors.red; debugPrint("$constraints"); if (constraints.maxHeight > 100) { color = Colors.blue; } return Container( height: 30, width: 50, color: color, ); }, ); }
json_serializable: ^3.5.0
person.dart
import 'package:json_annotation/json_annotation.dart'; part "person.g.dart"; @JsonSerializable(nullable: false) class Person { final String firstName; final String lastName; final DateTime dateOfBirth; Person({this.firstName, this.lastName, this.dateOfBirth}); factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json); Map<String, dynamic> toJson() => _$PersonToJson(this); }
cmd: flutter packages pub run build_runner build
=> generate person.g.dart
import 'package:json_annotation/json_annotation.dart'; part "person.g.dart"; @JsonSerializable(nullable: false) class Person { final String firstName; final String lastName; final DateTime dateOfBirth; Person({this.firstName, this.lastName, this.dateOfBirth}); factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json); Map<String, dynamic> toJson() => _$PersonToJson(this); }
或使用 json to code 工具(根据json生成dart code): https://app.quicktype.io/
或使用vscode 插件 Paste JSON as Code
https://jingyan.baidu.com/article/574c52196f55486c8d9dc106.html
lib/main.dart:
import 'dart:async'; import 'package:five_demo/pages/app/app_page.dart'; import 'package:five_demo/utils/log_utils.dart'; import 'package:flutter/material.dart'; import 'global/global.dart'; // void main() { // runApp(MyApp()); // } void main() async { // catch uncatched dart error runZonedGuarded(() async { Global.init().then((value) { // runApp(Global.wrapGlobalProviders(MyApp())); runApp(AppPage()); }); }, (error,stack)=>LogUtils.error("$error \n $stack")); }
lib/global/global.dart
import 'dart:io'; import 'package:five_demo/entities/platform_info.dart'; import 'package:five_demo/global/global_configs.dart'; import 'package:five_demo/utils/log_utils.dart'; import 'package:five_demo/utils/storage_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class Global{ static bool isFirstOpen; static PlatformInfo platformInfo; // static LoginUserInfo loginUserInfo; // static CheckInInfo checkInInfo; static Future init() async { LogUtils.info("[Global] init() start"); WidgetsFlutterBinding.ensureInitialized(); LogUtils.info("[Global] set FlutterErrorHander"); LogUtils.setFlutterErrorWidgetOnDebug(); LogUtils.setFlutterErrorHandlerOnRelease(); LogUtils.info("[Global] storage.init() start"); var storage = StorageUtils(); await storage.init(); LogUtils.info("[Global] storage.init() end"); /// isFirstOpen? isFirstOpen = storage.getBool(GlobalConfigs.Storage_App_First_Open,defalutValue:true); LogUtils.info("[Global] isFirstOpen: $isFirstOpen"); /// do after Welcome Page Loaded: // if(isFirstOpen){ // storage.setBool(GlobalConfigs.Storage_App_First_Open, false); // } // /// loginUser? => autoLogin // Map<String,dynamic> loginUserMap = storage.getMap(GlobalConfigs.Storage_App_Login_User,defalutValue: null); // if(loginUserMap!=null){ // loginUserInfo=LoginUserInfo.fromMap(loginUserMap); // /// Do auto login and update storage // LogUtils.info("[Global] Trigger autoLogin"); // // ... // } // LogUtils.info("[Global] LoginUserInfo: $loginUserInfo"); /// platformInfo platformInfo = PlatformInfo( operatingSystem: Platform.operatingSystem, operatingSystemVersion: Platform.operatingSystemVersion, version: Platform.version ); // android 状态栏为透明的沉浸 if (Platform.isAndroid) { SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent); SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); } LogUtils.info("[Global] PlatformInfo: $platformInfo"); // ... // await Future.delayed(Duration(seconds: 1)); LogUtils.info("[Global] init() end"); } }
pubspec.yaml:
dependencies: # 本地存储 shared_preferences: ^0.5.12+4
storage_utils.dart:
import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; class StorageUtils{ static final StorageUtils _instance = new StorageUtils._internal(); factory StorageUtils() => _instance; // 工厂构造 StorageUtils._internal(); // 命名构造 static SharedPreferences _prefs; Future<void> init() async { if (_prefs == null) { _prefs = await SharedPreferences.getInstance(); } } Future<bool> setMap(String key, Map<String,dynamic> map) async { String jsonStr = jsonEncode(map); return _prefs.setString(key, jsonStr); } Map<String,dynamic> getMap(String key,{Map<String,dynamic> defalutValue}) { String jsonStr = _prefs.getString(key); return jsonStr == null ? defalutValue : jsonDecode(jsonStr); } Future<bool> setBool(String key, bool val) { return _prefs.setBool(key, val); } bool getBool(String key,{bool defalutValue=false}) { bool val = _prefs.getBool(key); return val == null ? defalutValue : val; } Future<bool> remove(String key) { return _prefs.remove(key); } }
使用(global.dart)
import 'dart:io'; import 'package:five_demo/utils/storage_utils.dart'; class Global{ static bool isFirstOpen; // static LoginUserInfo loginUserInfo; static Future init() async { LogUtils.info("[Global] init() start"); WidgetsFlutterBinding.ensureInitialized(); var storage = StorageUtils(); await storage.init(); LogUtils.info("[Global] storage.init() end"); /// isFirstOpen? isFirstOpen = storage.getBool(GlobalConfigs.Storage_App_First_Open,defalutValue:true); LogUtils.info("[Global] isFirstOpen: $isFirstOpen"); /// do after Welcome Page Loaded: // if(isFirstOpen){ // storage.setBool(GlobalConfigs.Storage_App_First_Open, false); // } // /// loginUser? => autoLogin // Map<String,dynamic> loginUserMap = storage.getMap(GlobalConfigs.Storage_App_Login_User,defalutValue: null); // if(loginUserMap!=null){ // loginUserInfo=LoginUserInfo.fromMap(loginUserMap); // /// Do auto login and update storage // LogUtils.info("[Global] Trigger autoLogin"); // // ... // } // LogUtils.info("[Global] LoginUserInfo: $loginUserInfo"); // ... LogUtils.info("[Global] init() end"); } }
Global.isFirstOpen?WelcomePage():IndexPage()
isGreyFilter
使用Provider状态管理 (MVVM模式)pubspec.yaml
dependencies: # 状态管理 provider: ^4.3.2+2
app_page.dart
import 'package:five_demo/global/global.dart'; import 'package:five_demo/global/global_routes.dart'; import 'package:five_demo/global/global_themes.dart'; import 'package:five_demo/pages/pages.dart'; import 'package:five_demo/utils/log_utils.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'app_state.dart'; class AppPage extends StatefulWidget { final AppState appState = AppState.getInstance(); final Map args; AppPage({Key key,this.args}) : super(key: key); @override _AppPageState createState() => _AppPageState(); } class _AppPageState extends State<AppPage> { @override void initState() { debugPrint("+++ AppPage.initState() +++"); super.initState(); } @override void dispose() { debugPrint("+++ AppPage.dispose() +++"); super.dispose(); } @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<AppState>.value( value: widget.appState, ), ], child: _selectIsGrey(), ); } _selectIsGrey(){ return Selector<AppState,bool>( selector: (_,appState)=>appState.isGreyFilter, builder: (_,value,child){ return value? ColorFiltered( colorFilter: ColorFilter.mode(Colors.white, BlendMode.color), child: _buildMaterialApp() ):_buildMaterialApp(); } ); } _buildMaterialApp(){ return MaterialApp( title: 'Hey,Dear!', theme: GlobalThemes.mainThemeData, debugShowCheckedModeBanner:LogUtils.isInDebugMode, onGenerateRoute: GlobalRoutes.router.generator, home: Global.isFirstOpen?WelcomePage():IndexPage(), ); } }
app_state.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AppState with ChangeNotifier{ static final AppState _instance = AppState._internal(); factory AppState.getInstance(){ debugPrint("++++ 1. AppState.getInstance() +++"); return _instance; } AppState._internal(){ debugPrint("++++ 2. AppState._internal() +++"); } @override dispose(){ debugPrint("++++ 3. AppState.dispose() +++"); super.dispose(); } bool _isGreyFilter=false; /// isGreyFilter 灰色滤镜 get isGreyFilter { return _isGreyFilter; } set isGreyFilter(String value) { isGreyFilter = value; notifyListeners(); } switchGrayFilter(){ _isGreyFilter=!_isGreyFilter; notifyListeners(); } }
global_themes.dart
class GlobalThemes{ static ThemeData mainThemeData= ThemeData( primaryColor: Colors.white, visualDensity: VisualDensity.adaptivePlatformDensity, // 去除水波纹 highlightColor: Colors.transparent, splashColor:Colors.transparent, ); }
welcome_page.dart
import 'package:five_demo/global/global_configs.dart'; import 'package:five_demo/global/global_routes.dart'; import 'package:five_demo/utils/storage_utils.dart'; import 'package:flutter/material.dart'; class WelcomePage extends StatelessWidget { final Map args; WelcomePage({Key key,this.args}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: Container( width: double.infinity, decoration: BoxDecoration( image: DecorationImage( image: new AssetImage("assets/images/default/记录.png"), ) ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children:[ _buildTitle(context), // Image.network("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3103964819,2015218737&fm=26&gp=0.jpg"), // Image.asset("assets/images/default/记录.png"), _buildNextBtn(context) ] ) ), ); } _buildTitle(context){ return Text("Welcome",style: Theme.of(context).textTheme.headline4); } _buildNextBtn(context){ return IconButton( icon: Icon(Icons.arrow_forward), onPressed: (){ StorageUtils().setBool(GlobalConfigs.Storage_App_First_Open, false); // Navigator.of(context).pushNamed(GlobalRoutes.Route_Index); Navigator.of(context).pushNamedAndRemoveUntil(GlobalRoutes.Route_Index, (route) => false); } ); } }
使用Fluro动态路由框架
pubspec.yaml
dependencies: # 动态路由 fluro fluro: 1.7.7
global_routes.dart
import 'package:five_demo/pages/do/do_page.dart'; import 'package:fluro/fluro.dart'; import 'package:five_demo/pages/pages.dart'; class GlobalRoutes{ // Index,Welcome,Home,Do,My,Detail,Login,Error static const String Route_App = "/"; static const String Route_Index = "/index"; static const String Route_Welcome = "/welcome"; static const String Route_Home = "/home"; static const String Route_Do = "/do"; static const String Route_My="/my"; static const String Route_Detail ="/detail/:id"; static const String Route_Login = "/login"; static const String Route_Mall="/mall"; static const String Route_Error = "/error"; static const List<String> routePaths=[ Route_App, Route_Index, Route_Welcome, Route_Home, Route_Do, Route_My, Route_Detail, Route_Login, Route_Error ]; static FluroRouter router = init(); static init(){ FluroRouter router = FluroRouter(); routePaths.forEach((element) { router.define( element, handler: buildHandler(element), transitionType: TransitionType.native ); }); router.notFoundHandler=buildHandler(Route_Error); return router; } static buildHandler(String path){ return Handler( handlerFunc: (context,params)=>getRoutePageWidget(path, context.settings.arguments) ); } static getRoutePageWidget(String path,Map args){ // Index,Welcome,Home,Do,My,Detail,Login,Error switch(path){ case Route_App: return AppPage(args:args); case Route_Index: return IndexPage(args: args); case Route_Welcome: return WelcomePage(args:args); case Route_Home: return HomePage(args: args); case Route_Do: return DoPage(args:args); case Route_My: return MyPage(args:args); case Route_Detail: return DetailPage(args:args); case Route_Login: return LoginPage(args:args); case Route_Mall: return MallPage(args:args); default: return ErrorPage(args:args); } } }
app_page.dart
class AppPage extends StatefulWidget { final AppState appState = AppState.getInstance(); final Map args; AppPage({Key key,this.args}) : super(key: key); @override _AppPageState createState() => _AppPageState(); } class _AppPageState extends State<AppPage> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { // ... // _buildMaterialApp(); } _buildMaterialApp(){ return MaterialApp( title: 'Hey,Dear!', theme: GlobalThemes.mainThemeData, debugShowCheckedModeBanner:LogUtils.isInDebugMode, onGenerateRoute: GlobalRoutes.router.generator, // route! home: Global.isFirstOpen?WelcomePage():IndexPage(), ); } }
使用:
// welcome_page.dart: onPressed: (){ StorageUtils().setBool(GlobalConfigs.Storage_App_First_Open, false); // Navigator.of(context).pushNamed(GlobalRoutes.Route_Index); Navigator.of(context).pushNamedAndRemoveUntil(GlobalRoutes.Route_Index, (route) => false); } // banner_view.dart onTap: (index){ debugPrint("banner onTap:$index ${value.bannerItems[index].targetPath}"); Navigator.pushNamed( ctx, value.bannerItems[index].targetPath, arguments:{'title':value.bannerItems[index].title} ); } // channel_view.dart onTap: () => Navigator .of(context) .pushNamed( GlobalRoutes.Route_Detail, arguments: {'item':e,'targetType':e.targetType,'targetPath':e.targetPath} ) // channel_fm_index.dart onTap: (){ Navigator.pushNamed( context, GlobalRoutes.Route_Detail, arguments: {'item':item,'targetPath':item['targetPath'],'targetType':item['targetType']} ); } // pop onPressed: () => Navigator.of(context).pop() onPressed: () => Navigator.pop(context); onPressed: () => Navigator.pop(context,controller.text);
使用 BottomAppBar & IndexedStack & Provider(MVVM)
pubspec.yaml
dependencies: # 状态管理 provider: ^4.3.2+2
index_page.dart
class IndexPage extends StatefulWidget { final IndexState indexState = IndexState.getInstance(); final Map args; IndexPage({Key key,this.args}) : super(key: key); @override _IndexPageState createState() => _IndexPageState(); } class _IndexPageState extends State<IndexPage> { @override void initState() { debugPrint("+++ IndexPage.initState() +++"); super.initState(); } @override void dispose() { debugPrint("+++ IndexPage.dispose() +++"); super.dispose(); } @override Widget build(BuildContext context) { ScreenUtil.init( context, designSize:GlobalConfigs.ScreenDesignSize, allowFontScaling:true); return MultiProvider( providers: [ ChangeNotifierProvider<IndexState>.value(value: widget.indexState), ], child: Scaffold( /// header // appBar: wds.buildAppBar("Index"), /// body body: _selectCurrentIndex(_bodyBuilder), /// bottom bottomNavigationBar: BottomAppBar( child: Column( mainAxisSize: MainAxisSize.min, children: [ _selectCurrentIndex(_bottomNavBuilder), ], ), ), /// end ), ); } _selectCurrentIndex(builder){ return Selector<IndexState,int>( selector: (_,indexState)=>indexState.currentIndex, builder:builder ); } Widget _bodyBuilder(BuildContext context,int value,Widget child){ return IndexedStack( index: value, children: IndexState.items.map((e) => e.page).toList(), ); } Widget _bottomNavBuilder(BuildContext context,int value,Widget child){ return Container( padding: GlobalThemes.containerPadding, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.end, children: IndexState.items.map( (e) => InkWell( child:_buildBottomNavItem(e,value), onTap: ()=>_onTapHandler(e.index), ) ).toList() ), ); } _onTapHandler(index){ if (widget.indexState.currentIndex != index) { debugPrint("$index"); widget.indexState.currentIndex = index; } } Widget _buildBottomNavItem(IndexPageItem item,int currentIndex) { // debugPrint("_buildBottomMenuItem $index active: $active "); bool active = currentIndex==item.index; if (item.iconData == null) { return Container( padding: GlobalThemes.containerCirclePadding, decoration: IndexThemes.getBottomBarCircleDecoration(active), child: Text( item.label, style: IndexThemes.getBottomBarCircleTextStyle(active) ), ); } return Theme( data: ThemeData( iconTheme: IndexThemes.getBottomBarIconThemeData(active) ), child:Column( mainAxisSize: MainAxisSize.min, children: [ Icon(active?item.activeIconData:item.iconData), Text( item.label, style: IndexThemes.getBottomBarLabelTextStyle(active) ) ], ) ); } }
index_state.dart
class IndexPageItem{ int index; IconData iconData; IconData activeIconData; String label; Widget page; IndexPageItem({this.index,this.iconData,this.activeIconData,this.label,this.page}); } class IndexState with ChangeNotifier{ static List<IndexPageItem> items=[ IndexPageItem( index: 0, iconData:Icons.home_outlined, activeIconData: Icons.home, label:"首页", page:HomePage(args:{'title':'首页'}) ), IndexPageItem( index: 1, // iconData:Icons.list, label:"Do", page:DoPage(args:{'title':'Do'}) ), IndexPageItem( index: 2, iconData:Icons.person_outline, activeIconData: Icons.person, label:"我的", page:MyPage(args:{'title':'我的'}) ), ]; int _currentIndex = 0; static final IndexState _instance = IndexState._internal(); factory IndexState.getInstance() => _instance; // IndexState._internal() { // _currentIndex = 0; // } IndexState._internal(); /// currentIndex get currentIndex { return _currentIndex; } set currentIndex(int value) { _currentIndex = value; notifyListeners(); } }
登录注册方式:
Flutter 中“倒计时”的那些事儿https://blog.csdn.net/weixin_45189747/article/details/103370289
Flutter倒计时/计时器的实现https://zhuanlan.zhihu.com/p/61970955
pubspec.yaml
dependencies: # 提示框 fluttertoast: ^7.1.6
login_page.dart
import 'dart:async'; import 'package:five_demo/global/global_values.dart'; import 'package:five_demo/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; class LoginPage extends StatefulWidget { final Map args; LoginPage({Key key,this.args}) : super(key: key); @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { String phoneNo; String verificationCode; GlobalKey<FormState> formGlobalKey = GlobalKey<FormState>(); final TextEditingController _phoneNoController = TextEditingController(); final TextEditingController _codeController = TextEditingController(); Timer _timer; int _timeCount = 60; startTimer(){ _timeCount=60; _timer = Timer.periodic(Duration(seconds: 1), (Timer timer){ if(_timeCount<=0){ debugPrint("倒计时结束"); _timer.cancel(); setState((){ _timer=null; }); }else{ setState((){ _timeCount-=1; }); } }); } getCodeText(){ if(_timer!=null && _timeCount>=0) return "${_timeCount}s"; return "获取验证码"; } @override void dispose() { super.dispose(); if (_timer != null) { debugPrint("销毁啦"); _timer.cancel(); _timer=null; } } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(), body: GestureDetector( onTap: (){ debugPrint("gesture onTap"); FocusScope.of(context).requestFocus(new FocusNode()); // FocusScopeNode currentFocus = FocusScope.of(context); // if (!currentFocus.hasPrimaryFocus && // currentFocus.focusedChild != null) { // FocusManager.instance.primaryFocus.unfocus(); // } }, child: Container( color: Colors.grey[50], child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ _buildHeader(), _buildForm(), SizedBox(height: 100,), _buildThirdPart(), ], ), ), ), bottomNavigationBar: _buildPolicy(), ); } _buildAppBar(){ return AppBar( backgroundColor: Colors.grey[50], elevation: 0.0, toolbarHeight: 40, ); } _buildHeader(){ return Container( padding: EdgeInsets.only(top:10,bottom: 30), child: Text("Hey,Dear",style: TextStyle(fontSize: 22,fontWeight: FontWeight.w500,color: Colors.deepOrange)), ); } _buildForm(){ return Container( padding: EdgeInsets.symmetric(horizontal: 30), // height: 300, child: Form( key: formGlobalKey, child: Column( // padding: EdgeInsets.all(20), children: [ _buildPhoneNoField(), _buildCodeField(), _buildSubmit(), ], ), ), ); } _buildPhoneNoField(){ return Container( height: 45, padding: EdgeInsets.symmetric(horizontal: 10), margin: EdgeInsets.only(top: 10), decoration: BoxDecoration( // borderRadius: BorderRadius.circular(10), // border: Border.all(color: Colors.grey,), border: Border(bottom: BorderSide(color: Colors.grey)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(" + 86 |",style: TextStyle(fontSize: 14),), Expanded( child:TextFormField( controller: _phoneNoController, decoration: InputDecoration( hintText: "手机号码", hintStyle: TextStyle(fontSize: 14,color: Colors.grey), isDense: true, contentPadding: EdgeInsets.only(left: 10,top: 5,bottom: 0,right: 10), border: const OutlineInputBorder( gapPadding: 0, borderSide: BorderSide(width: 0,style: BorderStyle.none,), ), suffix: InkWell( onTap: (){ _phoneNoController.clear(); }, child: Icon(Icons.clear,size: 14,), ) ), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(11) ], // onSaved: (value){ // debugPrint("phoneNo onSaved"); // this.phoneNo=value; // }, )), // IconButton(icon: Icon(Icons.clear,size: 16,), onPressed: (){}) ], ), ); } _buildCodeField(){ return Container( height: 45, padding: EdgeInsets.only(left: 10), margin: EdgeInsets.only(top: 10), decoration: BoxDecoration( // borderRadius: BorderRadius.circular(10), // border: Border.all(color: Colors.grey,), border: Border(bottom: BorderSide(color: Colors.grey)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child:TextFormField( controller: _codeController, decoration: InputDecoration( hintText: "验证码", hintStyle: TextStyle(fontSize: 14,color: Colors.grey), isDense: true, contentPadding: EdgeInsets.only(left: 10,top: 5,bottom: 0,right: 10), border: const OutlineInputBorder( gapPadding: 0, borderSide: BorderSide(width: 0,style: BorderStyle.none,), ), ), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(8), ], // onSaved: (value){ // debugPrint("verificationCode onSaved"); // this.verificationCode=value; // }, )), _buildCodeBtn() ], ), ); } _buildCodeBtn(){ return Container( // height: 35, // decoration: BoxDecoration( // borderRadius: BorderRadius.circular(20), // color: Colors.grey[200], // ), child: FlatButton( onPressed: (){ if(_phoneNoController.text==null || _phoneNoController.text.length!=11){ Fluttertoast.showToast( msg:"请先输入手机号!", toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: Colors.grey[100], fontSize: 14, textColor: Colors.deepOrange[300] ); return; } if(_timer==null) startTimer(); else debugPrint("等等"); }, child: Text(getCodeText(),style: TextStyle(color: Colors.deepOrange[300],fontSize: 14),), ), ); } _buildSubmit(){ return Container( width: double.infinity, margin: EdgeInsets.only(top: 20), child: RaisedButton( child: Text("登 录", style: TextStyle(color: Colors.white,fontSize: 16)), color: Colors.deepOrange.withOpacity(0.8), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), onPressed: () { debugPrint("Submit Clicked"); // formGlobalKey.currentState.save(); // debugPrint("Submit get username: $username | password: $password"); bool result = formGlobalKey.currentState.validate(); debugPrint("validate result: $result"); if (result) { formGlobalKey.currentState.save(); debugPrint("Submit get phoneNo: $phoneNo | verificationCode: $verificationCode"); debugPrint("Submit controller phoneNo: ${_phoneNoController.text} | verificationCode: ${_codeController.text}"); } }, ), ); } _buildPolicy(){ return Container( padding: EdgeInsets.symmetric(vertical: 10), child: Text.rich(TextSpan( style: TextStyle(fontSize: 12,color: Colors.grey), children: [ TextSpan(text:"首次登录会自动创建账号,且代表同意"), TextSpan(text: "《用户服务协议》《隐私政策》",style: TextStyle(color: Colors.deepOrange)), ] )) ); } /* --------------------- */ _buildThirdPart(){ return Container( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ Text("使用第三方账号登录",style: TextStyle(color: Colors.grey),), SizedBox(height: 15,), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton(icon: Icon(Iconfont.wechat,color: Colors.green,size:36), onPressed: (){}), IconButton(icon: Icon(Iconfont.qq, color: Colors.blue,size:36), onPressed: (){}), IconButton(icon: Icon(Iconfont.weibo, color: Colors.red,size:36), onPressed: (){}), ], ) ], ), ); } }
pubspec.yaml
dependencies: # 轮播图 flutter_swiper: ^1.1.6 # 状态管理 provider: ^4.3.2+2 # http - dio dio: ^3.0.10 dio_log : ^1.3.3
index_page -> home_page: View(banner_view),VM(banner_state),M(banner_item)
index_page.dart
class IndexPage extends StatefulWidget { final IndexState indexState = IndexState.getInstance(); final BannerState bannerState = BannerState.getInstance(); final ActivityState activityState = ActivityState.getInstance(); // ... } class _IndexPageState extends State<IndexPage> { // ... @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<IndexState>.value(value: widget.indexState), ChangeNotifierProvider<BannerState>.value(value: widget.bannerState), ChangeNotifierProvider<ActivityState>.value(value: widget.activityState), ], child: Scaffold( body: _selectCurrentIndex(_bodyBuilder), // ... ), ); } }
home_page.dart
class HomePage extends StatefulWidget { final Map args; HomePage({Key key, this.args}) : super(key: key); @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: wds.buildAppBar(widget.args != null ? widget.args['title'] ?? '' : ''), // body: _buildBody(context), body: _buildWrapRefresh(context) ); } _onRefresh(BuildContext context) { context.read<CheckInState>().doCheckIn(); context.read<BannerState>().load(); context.read<ActivityState>().reload(); // ... } _buildWrapRefresh(BuildContext ctx) { return RefreshIndicator( onRefresh: () async { debugPrint("+++ HomePage onRefresh! +++"); _onRefresh(ctx); }, child: _buildBody(ctx)); } _buildBody(context) { return ListView( controller: Provider.of<ActivityState>(context).scrollController, children: [ Consumer<CheckInState>( builder: (_, value, child) => CheckInView(checkInState: value), ), Consumer<BannerState>(builder: (_, value, child) => BannerView(bannerState: value,)), Consumer<ActivityState>(builder: (_, value, child) => ActivityView()), // ... ], ); } }
banner_view.dart
import 'package:five_demo/pages/home/state/banner_state.dart'; import 'package:five_demo/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_swiper/flutter_swiper.dart'; import 'package:provider/provider.dart'; class BannerTheme{ static Color activePaginationColor = Colors.lightBlue[100]; static double bannerHeight=150; } class BannerView extends StatelessWidget { final BannerState bannerState; BannerView({Key key,this.bannerState}) : super(key: key); @override Widget build(BuildContext context) { debugPrint("--- BannerView Build ---"); // var value = Provider.of<BannerState>(context); return _buildBody(context, bannerState); } _buildBody(ctx,value){ if(value.isLoading){ return buildLoading(height:BannerTheme.bannerHeight); } if(value.bannerItems==null || value.bannerItems.length==0){ return buildEmpty(); } return _buildContent(ctx,value); } _buildContent(ctx,value){ return Container( height: BannerTheme.bannerHeight, child: Swiper( itemCount: value.bannerItems.length, itemBuilder: (context, index) { return Image.network(value.bannerItems[index].imageUrl, fit: BoxFit.cover); }, pagination: SwiperPagination( builder: DotSwiperPaginationBuilder(activeColor: BannerTheme.activePaginationColor) ), autoplay: true, autoplayDelay:3000, onTap: (index){ debugPrint("banner onTap:$index ${value.bannerItems[index].targetPath}"); Navigator.pushNamed( ctx, value.bannerItems[index].targetPath, arguments:{'title':value.bannerItems[index].title} ); } ) ); } }
banner_state.dart
import 'package:five_demo/entities/banner_item.dart'; import 'package:five_demo/entities/response_entity.dart'; import 'package:flutter/material.dart'; class BannerState with ChangeNotifier{ List<BannerItem> bannerItems; bool isLoading; static final BannerState _instance = BannerState._internal(); factory BannerState.getInstance() => _instance; BannerState._internal(){ load(); } @override dispose(){ debugPrint("-- BannerState dispose --"); bannerItems=null; super.dispose(); } load() async { // HttpUtils.get(HttpConfig.HomeBannerRequestPath) // .then((response){ // if(response.statusCode==200){ // var data = ResponseEntity<List<Map<String,dynamic>>>.fromMap(response.data['data']); // debugPrint("${data.toJson()}"); // bannerItems=data.result?.map((item) => BannerItem.fromMap(item)); // notifyListeners(); // } // }); Map<String,dynamic> responseData={ 'success':false, 'result':[ { 'id':'1', 'imageUrl':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2386581431,1215568659&fm=26&gp=0.jpg', 'title':'bannerPic1', 'subtitle':'this is banner1 picture', 'targetPath':'/detail/a', 'seqNo':'1' }, { 'id':'2', 'imageUrl':'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=35517157,3504012963&fm=26&gp=0.jpg', 'title':'bannerPic2', 'subtitle':'this is banner2 picture', 'targetPath':'/detail/b', 'seqNo':'2' }, { 'id':'3', 'imageUrl':'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1123133791,1776909836&fm=26&gp=0.jpg', 'title':'bannerPic3', 'subtitle':'this is banner3 picture', 'targetPath':'/detail/c', 'seqNo':'3' }, ] }; isLoading=true; notifyListeners(); await Future.delayed(Duration(seconds: 2)); var data = ResponseEntity<List<Map>>.fromMap(responseData); bannerItems=data.result.map( (item){ // debugPrint("$item"); return BannerItem.fromMap(item); } ).toList(); isLoading=false; notifyListeners(); } }
banner_item.dart
class BannerItem{ String id; String imageUrl; String title; String subtitle; String targetPath; Map<String,dynamic> args; int seqNo; BannerItem.fromMap(Map<String, dynamic> map){ id=map['id']; imageUrl=map['imageUrl']; title=map['title']; subtitle=map['subtitle']; targetPath=map['targetPath']; args=map['args']; seqNo=int.parse(map['seqNo']??"0"); } }
http_utils.dart
import 'package:dio/dio.dart'; import 'package:dio_log/interceptor/dio_log_interceptor.dart'; class HttpUtils { Dio dio; static final HttpUtils _instance = new HttpUtils._internal(); factory HttpUtils() => _instance; // 工厂构造 HttpUtils._internal() { BaseOptions options = new BaseOptions( contentType: 'application/json; charset=utf-8', headers: {'Authorization': null}); dio = Dio(options); // Interceptor interceptor = InterceptorsWrapper( // onRequest: (RequestOptions options) => options, // onResponse: (Response response) => response, // onError: (DioError error) => error, // ); // _dio.interceptors.add(interceptor); dio.interceptors.add(DioLogInterceptor()); // showDebugBtn(context); } static String getPath(String path,Map<String,dynamic> args){ // print("input path:$path"); Pattern pattern = RegExp(r':([^/|?|:]+)|([*])'); String result = path.replaceAllMapped( pattern, (match){ print("match group: ${match.group(0)}"); print("match group: ${match.group(1)}"); return "${args[match.group(1)]??match.group(0)}"; } ); // print(result); return result; } Future get( String path, { Map<String, dynamic> queryParameters, Options options, CancelToken cancelToken, ProgressCallback onReceiveProgress, }) async { return dio.get(path, queryParameters: queryParameters, options:options, cancelToken:cancelToken, onReceiveProgress:onReceiveProgress ); } // get dio => _dio; }
index_page -> home_page : View(activity_view),VM(activity_state),M(activity_item & link_item)
index_page.dart
class IndexPage extends StatefulWidget { final IndexState indexState = IndexState.getInstance(); final BannerState bannerState = BannerState.getInstance(); final ActivityState activityState = ActivityState.getInstance(); // ... } class _IndexPageState extends State<IndexPage> { // ... @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<IndexState>.value(value: widget.indexState), ChangeNotifierProvider<BannerState>.value(value: widget.bannerState), ChangeNotifierProvider<ActivityState>.value(value: widget.activityState), // ... ], child: Scaffold( body: _selectCurrentIndex(_bodyBuilder), // ... ), ); } }
home_page.dart
class HomePage extends StatefulWidget { final Map args; HomePage({Key key, this.args}) : super(key: key); @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: wds.buildAppBar(widget.args != null ? widget.args['title'] ?? '' : ''), // body: _buildBody(context), body: _buildWrapRefresh(context) ); } _onRefresh(BuildContext context) { context.read<CheckInState>().doCheckIn(); context.read<BannerState>().load(); context.read<ActivityState>().reload(); // ... } _buildWrapRefresh(BuildContext ctx) { return RefreshIndicator( onRefresh: () async { debugPrint("+++ HomePage onRefresh! +++"); _onRefresh(ctx); }, child: _buildBody(ctx)); } _buildBody(context) { return ListView( controller: Provider.of<ActivityState>(context).scrollController, children: [ Consumer<CheckInState>( builder: (_, value, child) => CheckInView(checkInState: value), ), Consumer<BannerState>(builder: (_, value, child) => BannerView(bannerState: value,)), Consumer<ActivityState>(builder: (_, value, child) => ActivityView()), // ... ], ); } }
activity_view.dart
import 'package:five_demo/pages/home/state/activity_state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class ActivityTheme{ // ... } class ActivityView extends StatelessWidget { ActivityView({Key key}) : super(key: key); @override Widget build(BuildContext context) { var value = Provider.of<ActivityState>(context); return _buildBody(context, value); } _buildBody(context,value){ // if(value.isLoading){ // return buildMsg("Loading..."); // } // if(value.activityItems==null || value.activityItems.length==0){ // return buildEmpty(); // } return _buildContent(context,value); } _buildContent(context,value){ return Container( margin: EdgeInsets.all(8), padding: EdgeInsets.all(8), // color: Colors.lightBlue[50], child: Column( children: [ _buildHeader(), SizedBox(height: 10), _buildItems(value), ], ) ); } _buildHeader(){ // ... } _buildItems(value){ return ListView.builder( itemCount: value.activityItems.length+1, shrinkWrap: true, physics: new NeverScrollableScrollPhysics(), itemBuilder: (ctx,index){ if(index==value.activityItems.length){ return _buildEndInfo(value); } return _buildActivity(value.activityItems[index]); } ); } _buildActivity(item){ // ... } _buildEndInfo(value){ var msg=""; if((value.activityItems==null || value.activityItems.length==0)){ if(value.isLoading) msg="More Loading ... "; else msg="No Data!"; }else{ if(value.isLoading) msg="More Loading ... "; else{ msg="End!"; } } return Container( padding: EdgeInsets.all(10), alignment: Alignment.center, child: Text(msg,style: TextStyle(fontSize: 14,color: Colors.grey),) ); } }
activity_state.dart
import 'package:five_demo/entities/activity_item.dart'; import 'package:five_demo/entities/response_entity.dart'; import 'package:flutter/material.dart'; class ActivityState with ChangeNotifier{ List<ActivityItem> activityItems; bool isLoading; int start=0; int limit=10; ScrollController scrollController; static final ActivityState _instance = ActivityState._internal(); factory ActivityState.getInstance() => _instance; ActivityState._internal(){ scrollController=new ScrollController(); scrollController.addListener(() { // debugPrint("_scrollController:${_scrollController.position.pixels}"); if(scrollController.position.pixels==scrollController.position.maxScrollExtent){ debugPrint("-- ActivityState ScrollController Trigger load --"); load(); } }); activityItems=[]; load(); } @override dispose(){ debugPrint("-- ActivityState dispose --"); activityItems=null; super.dispose(); } reload(){ start=0; limit=10; activityItems.clear(); load(); } load() async { /** String id; String avatar; String postUser; String postTag; String postTime; String postFrom; String postContent; String postLink; int likes; int comments; String targetPath; */ isLoading=true; notifyListeners(); Map<String,dynamic> responseData={ 'success':false, 'result':[ { 'id':'1', 'avatar':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2102119556,3949762144&fm=26&gp=0.jpg', 'postUser':'小甜甜', 'postTag':'官方', 'postFrom':'Hey!Dear', 'postTime':'2020-12-17', 'postContent':'喜洋洋与灰太狼的日常更新啦', 'postLink':{ 'cover':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1668316143,3125260297&fm=26&gp=0.jpg', 'title':'我在这里,我在这里', 'subtitle':'快来呀快来呀', 'targetPath':'/detail/link/1', 'linkType':'html' }, 'targetPath':'/detail/activity/1', 'likes':93840, 'comments':3 }, { 'id':'2', 'avatar':'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3224388312,1750026865&fm=26&gp=0.jpg', 'postUser':'肉嘟嘟', 'postTag':'官方', 'postFrom':'Hey!Dear', 'postTime':'2020-12-18', 'postContent':'健身啦健身啦', 'postLink':{ 'cover':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1832065145,3723585217&fm=26&gp=0.jpg', 'title':'全民运动节,礼品等你拿', 'targetPath':'/detail/link/2', 'linkType':'html' }, 'targetPath':'/detail/activity/2', 'likes':938, 'comments':0 }, { 'id':'3', 'avatar':'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1715123681,2767202506&fm=26&gp=0.jpg', 'postUser':'酷哥', 'postTag':'官方', 'postFrom':'Hey!Dear', 'postTime':'2020-12-18', 'postContent':'Rap! Rap! 跟着我的步伐', 'postLink':{ 'cover':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1996869834,1902662910&fm=26&gp=0.jpg', 'title':'嘿,你敢来么', 'targetPath':'/detail/link/3', 'linkType':'html' }, 'targetPath':'/detail/activity/3', 'likes':99, 'comments':9 }, ], }; isLoading=true; notifyListeners(); await Future.delayed(Duration(seconds: 2)); var data = ResponseEntity<List<Map>>.fromMap(responseData); activityItems.addAll( data.result.map( (item){ // debugPrint("$item"); return ActivityItem.fromMap(item); } ).toList() ); start+=limit; isLoading=false; notifyListeners(); } }
activity_item.dart
import 'package:five_demo/entities/link_item.dart'; class ActivityItem{ String id; String avatar; String postUser; String postTag; String postTime; String postFrom; String postContent; LinkItem postLink; int likes; int comments; String targetPath; ActivityItem.fromMap(Map map){ id=map['id']; avatar=map['avatar']; postUser=map['postUser']; postTag=map['postTag']; postFrom=map['postFrom']; postTime=map['postTime']; postContent=map['postContent']; postLink=map['postLink']!=null?LinkItem.fromMap(map['postLink']):null; likes=map['likes']; comments=map['comments']; targetPath=map['targetPath']; } Map toJson()=>{ 'id':id, 'avatar':avatar, 'postUser':postUser, 'postTag':postTag, 'postFrom':postFrom, 'postTime':postTime, 'postContent':postContent, 'postLink':postLink, 'likes':likes, 'comments':comments, 'targetPath':targetPath }; @override String toString() { return toJson().toString(); } }
link_item.dart
class LinkItem{ String cover; String title; String subtitle; String targetPath; String linkType; LinkItem.fromMap(Map map){ cover=map['cover']; title=map['title']; subtitle=map['subtitle']; targetPath=map['targetPath']; linkType=map['linkType']; } }
do_page
do_page.dart
class DoPage extends StatelessWidget { final Map args; DoPage({Key key,this.args}) : super(key: key); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<DoModuleState>(create: (_)=> DoModuleState.getInstance()), ChangeNotifierProvider<DoInfoState>(create:(_)=>DoInfoState.getInstance()), ChangeNotifierProvider<DoRecommendState>(create:(_)=>DoRecommendState.getInstance()), ], builder: (_,child){ return RefreshIndicator( onRefresh: () async { debugPrint("+++ DoPage onRefresh! +++"); _onRefresh(_); }, child: child ); }, child: Scaffold( // appBar: buildAppBar("Do"), appBar: _buildDoBar(), body: ListView( children: [ Consumer<DoModuleState>(builder: (_,value,child)=>DoModuleView(),), Consumer<DoRecommendState>(builder: (_,value,child)=>DoRecommendView(),), ], ), ), ); } _onRefresh(BuildContext context) { Provider.of<DoModuleState>(context,listen: false).load(); Provider.of<DoInfoState>(context,listen: false).load(); Provider.of<DoRecommendState>(context,listen: false).load(); } _buildDoBar(){ // ... } }
do_module_view.dart
class DoModuleView extends StatelessWidget { DoModuleView({Key key}) : super(key: key); @override Widget build(BuildContext context) { var value = Provider.of<DoModuleState>(context); return _buildBody(context, value); } _buildBody(BuildContext context,DoModuleState value){ if(value.isLoading){ return buildMsg("Loading..."); } if(value.doModules==null || value.doModules.length==0){ return buildEmpty(); } return _buildContent(context,value); } _buildContent(BuildContext context,DoModuleState value){ return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: value.doModules.map<Widget>( (e) =>_buildModule(context,e) ).toList(), ), ); } _buildModule(BuildContext context,DoModule module){ return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildModuleHeader(context,module), _buildModuleProjects(context,module.children) ], ), ); } _buildModuleHeader(BuildContext context,DoModule module){ // ... } _buildModuleProjects(BuildContext context,List<DoProject> items){ return buildPanel( Column( crossAxisAlignment: CrossAxisAlignment.start, children: ListTile.divideTiles( context: context, tiles: items.map<Widget>((e){ return Column( children: [ _buildProjectContent(e), _buildProjectItems(context,e.children), ], ); }) ).toList(), ) ); } _buildProjectContent(DoProject project){ return Selector<DoInfoState,DoInfoItem>( selector: (_,value){ if(value.doInfoMap!=null) return value.doInfoMap[project.id]; return null; }, builder: (context, info, child) { // debugPrint("build project ${project.id} cause info change"); return ListTile( dense: true, contentPadding: EdgeInsets.all(0), leading: _buildProjectLeading(project,info), title: Text(project.title,style: project.level==2?DoModuleViewTheme.level2Title:DoModuleViewTheme.level1Title), subtitle: _buildProjectSubtitle(info), trailing: _buildProjectTrailing(project,info), onTap: (){ debugPrint("do Project onTap: ${project.id} ${project.title}"); Navigator.pushNamed( context, GlobalRoutes.Route_Detail,arguments: {'item':project,'info':info,'targetPath':project.targetPath,'targetType':project.targetType} ); context.read<DoInfoState>()?.reset(project.id); }, ); }, ); } _buildProjectItems(BuildContext context,List<DoProject> items){ if(items==null || items.length==0) return Container(); return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: items.map<Widget>((e){ return Container( child: _buildProjectContent(e) ); }).toList(), ), ); } }
channel_fm_index.dart:
class ChannelFmIndex extends StatefulWidget { ChannelFmIndex({Key key}) : super(key: key); @override _ChannelFmIndexState createState() => _ChannelFmIndexState();}class _ChannelFmIndexState extends State<ChannelFmIndex> with SingleTickerProviderStateMixin { TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildHeader(), body: _buildTabView(context), floatingActionButton: _buildPopupMenu(), ); } _buildPopupMenu(){ return PopupMenuButton( offset: Offset(0,-110), icon: Icon(Icons.menu,color: Colors.black,), itemBuilder: (BuildContext context) => <PopupMenuEntry>[ const PopupMenuItem( child: FlatButton(onPressed: null, child: Text("@ 客服")) ), PopupMenuItem( child: FlatButton.icon( onPressed: null, icon: Icon(Icons.edit,size: 16,), label: Text("投稿") ) ), ] ); } _buildHeader(){ return AppBar( elevation: 0, automaticallyImplyLeading: false, titleSpacing: 0, toolbarHeight: 160, title: _buildHeadCard(), bottom: PreferredSize( preferredSize: Size.fromHeight(50), child: _buildHeadTabBar(), ), ); } _buildHeadCard(){ return Container( child: Row( children: [ // ... ], ), ); } /* ------------------------------------ */ _buildHeadTabBar(){ return Container( decoration: BoxDecoration( color: Colors.white, border: Border.symmetric( horizontal: BorderSide(color: Colors.grey[50],width: 8) ) ), child: TabBar( controller: _tabController, indicatorColor: Colors.black87, indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 1.0, tabs: [ Tab(child: Text("最新"),), Tab(child: Text("动态"),), Tab(child: Text("简介"),), ], ), ); } _buildTabView(BuildContext context){ return TabBarView( controller: _tabController, children: <Widget>[ _buildRefreshIndicator("Latest",_buildTabOfLatest(context)), _buildRefreshIndicator("Activity",_buildTabOfActivity(context)), _buildRefreshIndicator("Intro",_buildTabOfIntro()), ], ); } /* ------------------------------------ */ _buildRefreshIndicator(String name,Widget child){ return RefreshIndicator( onRefresh: () async { debugPrint("+++ $name onRefresh! +++"); }, child: child ); } _buildTabOfLatest(BuildContext context){ // ... } _buildTabOfActivity(context){ // ... } _buildTabOfIntro(){ // ... }}
https://pub.dev/packages/audioplayershttps://dengxiaolong.com/article/2019/07/how-to-play-audioplaxyers-in-flutter.htmlhttps://www.jianshu.com/p/288f869690f0音效库 https://www.tukuppt.com/yinxiao/m101/zonghe_0_0_0_0_0_0_3.html
pubspec.yaml
dependencies: # 音频播放 audioplayers: ^0.17.2
channel_media_index.dart:
class ChannelMediaIndex extends StatefulWidget { ChannelMediaIndex({Key key}) : super(key: key); @override _ChannelMediaIndexState createState() => _ChannelMediaIndexState();}class _ChannelMediaIndexState extends State<ChannelMediaIndex> with SingleTickerProviderStateMixin { TabController _tabController; AudioPlayer _audioPlayer; var _playingItem={}; Duration maxDuration; Duration currentDuration; AudioPlayerState playerState; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _initAudioPlayer(); } _initAudioPlayer() { _audioPlayer = new AudioPlayer(); _audioPlayer.onDurationChanged.listen((Duration d) { debugPrint('Max duration: $d'); if(mounted) setState(() => maxDuration = d); }); // 位置事件: 此事件更新音频的当前播放位置 _audioPlayer.onAudioPositionChanged.listen((Duration p) { // debugPrint('Current position: ${p.inMinutes}:${p.inSeconds}'); if(mounted) setState(() => currentDuration = p); }); // 状态事件: 此事件返回当前播放器状态。你可以用它来显示播放器是否在播放,或停止,或暂停 _audioPlayer.onPlayerStateChanged.listen((AudioPlayerState s) { debugPrint('Current player state: $s'); if(mounted) setState(() => playerState = s); }); // 完成状态: 当音频结束播放时调用此事件(当使用pause或者stop方法中断播放时不会产生该事件) _audioPlayer.onPlayerCompletion.listen((event) { debugPrint('Current player completed'); // setState(() { position = duration;}); }); // 错误事件: 当在本地代码中抛出意外错误时 _audioPlayer.onPlayerError.listen((msg) { print('audioPlayer error : $msg'); // setState(() { // playerState = PlayerState.stopped; // duration = Duration(seconds: 0); // position = Duration(seconds: 0); // }); }); } play(String resource, {bool isLocal = false}) async { int result = await _audioPlayer.play(resource, isLocal: isLocal); if (result == 1) { debugPrint('play success'); } else { debugPrint('play failed'); } } stop() async { int result = await _audioPlayer.stop(); if (result == 1) { debugPrint('stop success'); } else { debugPrint('stop failed'); } } pause() async { int result = await _audioPlayer.pause(); if (result == 1) { // success debugPrint('pause success'); } else { debugPrint('pause failed'); } } resume() async { int result = await _audioPlayer.resume(); if (result == 1) { debugPrint('resume success'); } else { debugPrint('resume failed'); } } seek(startMilliseconds) async { int result = await _audioPlayer.seek(new Duration(milliseconds: startMilliseconds)); if (result == 1) { debugPrint('seek: go to $startMilliseconds success'); // await audioPlayer.resume(); } else { debugPrint('seek: go to $startMilliseconds failed'); } } @override void deactivate() async { debugPrint('channel_media_index deactivate!'); int result = await _audioPlayer.release(); if (result == 1) { debugPrint('audioPlayer release success'); } else { debugPrint('audioPlayerrelease failed'); } super.deactivate(); } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar("FM:风姿物语 - 2020.08"), body: _buidBody(), ); } _buidBody() { return NestedScrollView( headerSliverBuilder: (_,innerBoxIsScrolled){ return [ SliverToBoxAdapter(child: _buildPlayPanel(),), SliverAppBar( automaticallyImplyLeading: false, elevation: 0, floating: true, pinned: true, title: _buildTabBar() ), ]; }, body: _buildTabView() ); } _buildPlayPanel() { return Container( height: 330, decoration: BoxDecoration( gradient: RadialGradient( radius: 1, colors: [Colors.brown,Colors.brown[300],Colors.brown[100]] ), ), child: Column( children: [ SizedBox(height: 20,), Expanded(child:_buildPlayCovers()), SizedBox(height: 10,), Text(_playingItem['title']!=null?_playingItem['title']:"",style: TextStyle(color: Colors.white.withOpacity(0.7),fontWeight: FontWeight.bold,fontSize: 16),), SizedBox(height: 10,), _buildPlayCtls() ], ), ); } _buildPlayCovers(){ var covers=[ "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2826031711,1618078016&fm=26&gp=0.jpg", "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1822980246,1716056569&fm=26&gp=0.jpg", "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3155864268,1264666725&fm=26&gp=0.jpg", "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1899644059,569818638&fm=26&gp=0.jpg" ]; return Swiper( itemCount: covers.length, itemBuilder: (_,index){ return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: NetworkImage(covers[index]), fit: BoxFit.fill, ), // color: Colors.grey, ), ); }, autoplay: false, viewportFraction: 0.8, scale: 0.9, ); } _buildPlayCtls(){ return Container( child: Column(children: [ Container( padding: EdgeInsets.symmetric(horizontal: 10), child: LinearProgressIndicator( value: (maxDuration!=null && currentDuration!=null)? currentDuration.inSeconds/maxDuration.inSeconds : 0, backgroundColor: Colors.white.withOpacity(0.2), valueColor: AlwaysStoppedAnimation<Color>(Colors.white.withOpacity(0.7)), minHeight:2, ), ), Container( padding: EdgeInsets.only(left: 10,right: 10,top: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(currentDuration!=null?"${currentDuration.inMinutes}:${currentDuration.inSeconds.remainder(60)}":"",style: TextStyle(fontSize: 12,color: Colors.white.withOpacity(0.5)),), Text(maxDuration!=null?"${maxDuration.inMinutes}:${maxDuration.inSeconds.remainder(60)}":"",style: TextStyle(fontSize: 12,color: Colors.white.withOpacity(0.5)),), ], ), ), Container( // color: Colors.grey, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ InkWell( child:Icon(Icons.fast_rewind,color: Colors.white.withOpacity(0.5)), onTap: (){}, ), InkWell( child: Icon(AudioPlayerState.PLAYING==playerState?Icons.play_arrow:Icons.pause,size: 34,color: Colors.white), onTap: (){ if(AudioPlayerState.PLAYING==playerState) pause(); else if(_playingItem!=null && _playingItem['resource']!=null) play(_playingItem['resource']); }, ), Icon(Icons.fast_forward,color: Colors.white.withOpacity(0.5)), ], ), ), SizedBox(height: 8,) ],) ); } /* ----------------- */ _buildTabBar() { return Container( decoration: BoxDecoration( color: Colors.white, border: Border.symmetric( horizontal: BorderSide(color: Colors.grey[50], width: 8))), child: TabBar( controller: _tabController, indicatorColor: Colors.black87, indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 1.0, tabs: [ Tab( child: Text("播放列表"), ), // 音频,点击 -> 当前页切换播放内容 Rebuild (同一路由) Tab( child: Text("花絮周边"), ), // 动态,图文/影片,评论留言 -> 底部弹框,关闭 Tab( child: Text("简介"), ) ], ), ); } _buildTabView() { return TabBarView( controller: _tabController, children: <Widget>[ _buildTab1(), _buildTab2(), _buildTab3(), ], ); } /* --------------------- */ getDataOfPlayList() { return [ { 'id': '01', 'title': '风中有朵雨做的云', 'duration': '2:30', 'subtitle': '收录于专辑《听说》', 'resource':'https://img.tukuppt.com/newpreview_music/08/98/97/5c88d1231eeb570304.mp3' }, { 'id': '02', 'title': '雨一直下', 'duration': '5:30', 'subtitle': '收录于专辑《听说》', 'resource': 'https://img.tukuppt.com/newpreview_music/09/00/75/5c894afe4de5f1618.mp3' }, { 'id': '03', 'title': '恶作剧幽默搞怪', 'duration': '02:03', 'subtitle': '猜猜是什么话题', // 'resource':'https://img.tukuppt.com/newpreview_music/09/00/44/5c8926ae5fdb676071.mp3', 'resource':'https://img.tukuppt.com/newpreview_music/09/00/75/5c894afdc4c1578647.mp3' }, {'id': '04', 'title': '舒缓音乐', 'duration': '12:30','resource':'https://img.tukuppt.com/newpreview_music/09/00/76/5c894c2e6ad5854338.mp3'}, {'id': '05', 'title': '海边悠扬的海浪拍打声和海鸥鸣叫声', 'duration': '3:30', 'subtitle': 'amazing!','resource':'https://img.tukuppt.com/newpreview_music/09/00/40/5c8921fb5b7a586734.mp3'}, ]; } _buildTab1() { return Container( child: ListView(children: [ _buildPlayList(getDataOfPlayList()), // ... ]) ); } _buildPlayList(playList) { return Container( color: Colors.white, padding: EdgeInsets.all(10), child: Column( children: ListTile.divideTiles( context: context, tiles: playList.map<Widget>((e) { bool isActive = (_playingItem!=null && _playingItem['id']==e['id']); return InkWell( child: Container( padding: EdgeInsets.all(8), child: Row( children: [ Icon(Icons.play_circle_outline,color: isActive?Colors.deepOrange:Colors.grey,), SizedBox(width:10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${e['title']}"), e['subtitle'] != null ? Text( "${e['subtitle']}", style: TextStyle( color: Colors.grey, fontSize: 12), ) : SizedBox() ], ), ), Text( "${e['duration']}", style: TextStyle(color: Colors.grey, fontSize: 12), ) ], ), ), onTap: (){ if(e['resource']==null) return; if(mounted){ setState(() { _playingItem=e; }); play(e['resource']); } }, ); })).toList(), ), ); }}
Flutter中setState导致的内存泄漏——setState() called after dispose()
https://blog.csdn.net/qq_26287435/article/details/89674247错误原因是异步消息未返回,所以在setState方法之前调用mouted属性进行判断即可。具体示例如下:
if(mounted){ setState(() { _listData.addAll(list); }}
Flutter 中使用 video_player 播放视频http://www.ptbird.cn/flutter-video.html
Flutter视频播放、Flutter VideoPlayer 视频播放组件精要https://blog.csdn.net/zl18603543572/article/details/111327310
pubspec.yaml
dependencies: # 视频播放 video_player: ^1.0.1
video_play_widget.dart:
import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class VideoPlayWidget extends StatefulWidget { final String url; VideoPlayWidget({Key key,this.url}) : super(key: key); @override _VideoPlayWidgetState createState() => _VideoPlayWidgetState(); } class _VideoPlayWidgetState extends State<VideoPlayWidget> { VideoPlayerController _playerController ; @override void initState() { super.initState(); _playerController = VideoPlayerController.network(widget.url); _playerController.initialize() //异步执行完的回调 ..whenComplete(() { //刷新页面 setState(() {}); }); } @override void dispose() { _playerController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { debugPrint("pop"); Navigator.pop(context); }, child: Container( color: Colors.black, alignment: Alignment.center, child: _buildPlayer(), ), ); } _buildPlayer(){ return AspectRatio( //设置视频的大小 宽高比。长宽比表示为宽高比。例如,16:9宽高比的值为16.0/9.0 aspectRatio: _playerController.value.aspectRatio, //播放视频的组件 child: VideoPlayer(_playerController), ); } }
调用:
Navigator.push(context, PopupRouteWidget(child: VideoPlayWidget( url: entry.value['link'] )) );
pubspec.yaml
dependencies: # 弹出放大缩小图片 photo_view: ^0.10.3
photo_preview_widget.dart:
import 'package:flutter/material.dart';import 'package:photo_view/photo_view.dart';import 'package:photo_view/photo_view_gallery.dart';class PhotoPreviewWidget extends StatefulWidget { final int initialIndex; final List photoList; final PageController pageController; PhotoPreviewWidget({Key key,this.initialIndex,this.photoList}) : pageController = PageController(initialPage: initialIndex),super(key: key); @override _PhotoPreviewWidgetState createState() => _PhotoPreviewWidgetState();}class _PhotoPreviewWidgetState extends State<PhotoPreviewWidget> { int _currentIndex; @override void initState() { _currentIndex = widget.initialIndex; super.initState(); } void onPageChanged(int index) { setState(() { _currentIndex = index; }); } @override Widget build(BuildContext context) { return Container( color: Colors.black, child: Stack( children: [ GestureDetector( onTap: () => Navigator.pop(context), child: _buildPhotoPreview(), ), Align( alignment: Alignment.bottomCenter, child: _buildPagination(), ) ], ), ); } _buildPhotoPreview(){ return Container( child: PhotoViewGallery.builder( scrollPhysics: const BouncingScrollPhysics(), onPageChanged: onPageChanged, itemCount: widget.photoList.length, pageController: widget.pageController, loadingBuilder: (context, event) => Center( child: Container( width: 20.0, height: 20.0, child: CircularProgressIndicator( value: event == null ? 0 : event.cumulativeBytesLoaded / event.expectedTotalBytes, ), ), ), builder: (BuildContext context, int index) { return PhotoViewGalleryPageOptions( imageProvider: NetworkImage(widget.photoList[index]['cover']), minScale: PhotoViewComputedScale.contained * 0.6, maxScale: PhotoViewComputedScale.covered * 1.1, initialScale: PhotoViewComputedScale.contained, ); }, ), ); } _buildPagination(){ return Container( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( widget.photoList.length, (i) => Container( margin: EdgeInsets.all(5), child: CircleAvatar( radius: 4, backgroundColor: _currentIndex == i ? Colors.white.withOpacity(0.9) : Colors.white.withOpacity(0.5), ), ), ).toList(), ), ); }}
点击触发展示底部评论框 (channel_media_index.dart / project_activity_view.dart)
FlatButton.icon( icon:Icon(Icons.comment,size:16,color:Colors.grey), label: Text("评论",style:TextStyle(fontSize:12,color:Colors.grey)), onPressed: (){ Navigator.push(context, PopupRouteWidget(child: InputBottomWidget( onEditingCompleteText: (text){ print('点击发送 ---$text'); }, )) ); }), FlatButton( onPressed: (){ Navigator.push(context, PopupRouteWidget(child: InputBottomWidget( draftText: activity['draftText'], onEditingCompleteText: (text){ print('点击发送 ---$text'); }, )) ).then((value){ print("input draft: $value --> cache local"); activity['draftText']=value; }); }, child: Text("评论") ); FlatButton( onPressed: (){ Navigator.push(context, PopupRouteWidget(child: InputBottomWidget( hintText: "回复 ${reply['from']}", onEditingCompleteText: (text){print('点击发送 ---$text');}, )) ); }, child: Text("回复") );
底部评论框 input_bottom_widget.dart (点击其他处/输入框回车发送 => 关闭):
import 'package:flutter/material.dart'; class InputBottomWidget extends StatelessWidget { final String hintText; final String draftText; final ValueChanged onEditingCompleteText; // typedef ValueChanged<T> = void Function(T value); final TextEditingController controller = TextEditingController(); InputBottomWidget({ Key key, this.onEditingCompleteText, this.hintText="评论", this.draftText }) : super(key: key) { controller.text=draftText; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, body: Column( children: <Widget>[ Expanded( // 蒙板 child: GestureDetector( child: Container( color: Colors.transparent, ), onTap: () { Navigator.pop(context,controller.text); // 关闭此页 }, ) ), Container( color: Colors.grey[100], padding: EdgeInsets.symmetric(vertical: 8,horizontal: 16), child: _buildInputTextField(context) ) ], ), ); } _buildInputTextField(context){ return Container( decoration: BoxDecoration( color: Colors.white ), child: TextField( decoration: InputDecoration( hintText: hintText, isDense: true, contentPadding: EdgeInsets.only(left: 10,top: 5,bottom: 5,right: 10), border: const OutlineInputBorder( gapPadding: 0, borderSide: BorderSide(width: 0,style: BorderStyle.none,), ), ), controller: controller, autofocus: true, style: TextStyle(fontSize: 16), //设置键盘按钮为发送 textInputAction: TextInputAction.send, keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, onEditingComplete: (){ //点击发送调用 print('onEditingComplete'); onEditingCompleteText(controller.text); Navigator.pop(context); }, ), ); } }
弹出消失动画 popup_route_widget.dart:
import 'package:flutter/material.dart'; class PopupRouteWidget extends PopupRoute{ final Duration _duration = Duration(milliseconds: 300); Widget child; PopupRouteWidget({@required this.child}); @override Color get barrierColor => null; @override bool get barrierDismissible => true; @override String get barrierLabel => null; @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { // return child; return FadeTransition( opacity: animation, child: child, ); } @override Duration get transitionDuration => _duration; }
pubspec.yaml
dependencies: # 瀑布流 flutter_staggered_grid_view: ^0.3.2
channel_daily_index.dart:
_buildNoteItems(){ var noteList=[ {'id':'1','cover':'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2248624002,2108517552&fm=26&gp=0.jpg','title':'采茶之旅','postBy':'砍柴人','avatar':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3397053345,592981929&fm=26&gp=0.jpg','likes':5}, {'id':'2','cover':'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3008435233,972423020&fm=26&gp=0.jpg','title':'断桥边','postBy':'砍柴人','avatar':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3397053345,592981929&fm=26&gp=0.jpg','likes':0}, {'id':'3','cover':'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2126854169,3585890470&fm=26&gp=0.jpg','title':'亲子厨房','postBy':'砍柴人','avatar':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3397053345,592981929&fm=26&gp=0.jpg','likes':15}, {'id':'4','cover':'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2487663409,412721828&fm=26&gp=0.jpg','title':'墙壁大作战','postBy':'砍柴人','avatar':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3397053345,592981929&fm=26&gp=0.jpg','likes':35}, {'id':'5','cover':'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3647241271,3241631924&fm=26&gp=0.jpg','title':'灰色灰色','postBy':'砍柴人','avatar':'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3397053345,592981929&fm=26&gp=0.jpg','likes':45}, ]; return Container( padding: EdgeInsets.symmetric(horizontal: 10), child: StaggeredGridView.countBuilder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), crossAxisCount: 4, itemCount: noteList.length, itemBuilder: (BuildContext context, int index) => _buildNoteContent(index, noteList[index]), staggeredTileBuilder: (int index) => new StaggeredTile.count(2, index.isEven ? 2:2.5), mainAxisSpacing: 8, crossAxisSpacing: 8, ), ); } _buildNoteContent(index,item){ // return Container( // color: Colors.white, // child: Center( // child: CircleAvatar( // backgroundColor: Colors.grey[100], // child: Text('$index'), // ), // ) // ); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: Colors.white, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: ClipRRect( borderRadius: BorderRadius.circular(5), child: Image.network(item['cover'],fit: BoxFit.cover,) ) ), SizedBox(height: 5,), Text(item['title'],style: TextStyle(fontWeight: FontWeight.w500),), SizedBox(height: 5,), Row( children: [ CircleAvatar(radius: 10,backgroundImage: NetworkImage(item['avatar']),), SizedBox(width: 5,), Expanded(child:Text(item['postBy'])), Icon(Icons.favorite_outline,size: 15,color: Colors.grey,), SizedBox(width:5,), Text("${item['likes']}") ], ), SizedBox(height: 5,), ], ), ); }
project_chat_view.dart:
class ProjectChatView extends StatefulWidget { final FocusNode focusNode; ProjectChatView({Key key,this.focusNode}) : super(key: key); @override _ProjectChatViewState createState() => _ProjectChatViewState();}class _ProjectChatViewState extends State<ProjectChatView> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; ScrollController _scrollController = ScrollController() ; final TextEditingController _inputController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( body: _buildChatList(getDataOfChatMsgs()), bottomNavigationBar:BottomAppBar( child: _buildInputPanel(), ) ); } _buildInputPanel(){ return Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey[200], ), child: Row(children: [ Expanded(child: _buildInputTextField()), SizedBox(width: 5,), Icon(Icons.insert_emoticon), SizedBox(width: 5,), InkWell( child:Icon(Icons.add_circle_outline), onTap: (){ // _chatScrollController.jumpTo(_chatScrollController.position.maxScrollExtent); // Future.delayed(Duration(milliseconds: 100), (){ // _scrollController.jumpTo(_scrollController.position.maxScrollExtent); // }); }, ), ],) // child: TextField(), ); } _buildInputTextField(){ return TextField( decoration: InputDecoration( isDense: true, hintText: '@ 管理员', hintStyle: TextStyle(color: Colors.grey), border: InputBorder.none, filled: true, fillColor: Colors.white, ), keyboardType: TextInputType.multiline, textInputAction: TextInputAction.send, minLines: 1, maxLines: 5, focusNode: widget.focusNode, controller: _inputController, onSubmitted: (value) { debugPrint('submit: $value'); _inputController.clear(); }, onTap: (){ debugPrint("tap"); Future.delayed(Duration(milliseconds: 100), (){ _scrollController.jumpTo(_scrollController.position.maxScrollExtent); // _scrollController.animateTo( // _scrollController.position.maxScrollExtent, // duration: Duration(milliseconds: 500), curve: Curves.ease); }); }, ); }}
https://pub.flutter-io.cn/packages/webview_flutter/example
Flutter WebView与JS交互简易指南https://blog.csdn.net/weixin_33739541/article/details/89571639
Flutter使用JsBridge方式处理Webview与H5通信的方法https://www.yht7.com/news/60783
Flutter 采坑之旅--webview_flutterhttps://www.jianshu.com/p/611840cc2797
pubspec.yaml
dependencies: webview_flutter: ^1.0.7
@overrideWidget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(), floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat, body: SafeArea( child: _buildWebview(), ), );}_buildWebview(){ return Container( height: _webViewHeight, child: WebView( initialUrl: widget.initialUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) { _controller.complete(webViewController); }, onPageStarted: (String url) { debugPrint('Page started loading: $url'); }, onPageFinished: (String url) { debugPrint('Page finished loading: $url'); _getWebViewHeight(); // setState(() { // // _isPageFinished = true; // }); }, gestureNavigationEnabled: true, javascriptChannels: <JavascriptChannel>[ _invokeJavascriptChannel(context), ].toSet(), //拦截页面url navigationDelegate: (NavigationRequest request) { if (request.url.startsWith('alipays:') || request.url.startsWith('weixin:')) { // _openPay(context, request.url); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, ), );}// 注册js回调JavascriptChannel _invokeJavascriptChannel(BuildContext context) { return JavascriptChannel( name: 'Invoke', onMessageReceived: (JavascriptMessage message) { debugPrint("${message.message}"); var webHeight = double.parse(message.message); if (webHeight != null) { setState(() { _webViewHeight = webHeight; }); } } );}// 获取页面高度_getWebViewHeight() async { WebViewController webviewController = await _controller.future; final String temp = await webviewController.getTitle(); debugPrint('title:' + temp); await webviewController.evaluateJavascript('Invoke.postMessage(document.documentElement.scrollHeight)');}
调用 (channel_media_index.dart)
WebviewWidget(initialUrl: "https://juejin.cn/post/6844903908087693319",)
html内容加载
https://pub.flutter-io.cn/packages/flutter_htmlhttps://cloud.tencent.com/developer/article/1502142https://www.jianshu.com/p/8ea17b06e541
pubspec.yaml
dependencies: # html flutter_html: ^1.0.0
_buildRecommendContent(String targetPath){ return Container( child: Html( data: ''' <ul> <li> <div> <img src="https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=30699094,3647522649&fm=26&gp=0.jpg"/> </div> <div style="color:green;border:5px solid red;"> Content </div> </li> </ul> ''', style: { "img":Style( width: 100 ), "div":Style( display: Display.INLINE ) }, ), ); }
Refer to swagger2 注解说明
请求类上:
@Api(tags="说明该类的作用")
请求方法上
@ApiOperation(value="",notes="")
说明方法的用途、作用@ApiImplicitParams({@ApiImplicitParam(xxxx),...})
,@ApiImplicitParam
参数说明name
:参数名value
:参数的汉字说明、解释required
:参数是否必须传paramType
:参数放在哪个地方@RequestHeader
获取@RequestParam
获取@PathVariable
获取dataType
:参数类型,默认String defaultValue
:参数的默认值@ApiResponses({@ApiResponse(xxx),...})
表示一组响应(一般用于表达错误的响应信息)code
:数字,例如400message
:信息,例如"请求参数没填好"response
:抛出异常的类请求响应entity上(若无特殊说明,不加注解也可,会使用默认的,即直接使用类名&字段名&类型):
@ApiModel
(一般用在post/put创建或修改时,使用@RequestBody的场景,请求参数无法使用@ApiImplicitParam
注解进行描述) 的时候)
@ApiModelProperty
:用在entity的属性上Sample:
@ApiOperation("删除某评论")@ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "id", dataType = "Integer", paramType = "path"), @ApiImplicitParam(name = "type", value = "type", dataType = "String", paramType = "path", allowableValues="home,school")})@ApiResponses({ @ApiResponse(code=400,message="请求参数没填好"), @ApiResponse(code=404,message="请求路径没有或页面跳转路径不对")})@PutMapping("/comments/{id}/{type}")public Object updateComment(@PathVariable("id") Integer id, @PathVariable("type")String type, @RequestBody Comment comment){ // ...}
dependencies
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency>
configuration
@Configuration @EnableSwagger2 public class SwaggerConfig { // @Value("${swagger.enable:false}") // private boolean enable=false; @Bean public Docket docket(Environment env) { Profiles profiles = Profiles.of("dev"); boolean flag = env.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(flag) // .enable(enable) .select() .apis(RequestHandlerSelectors.basePackage("com.cj.mybatis.controller")) //.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) //.paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Swagger Demo") .description("This for swagger demo") //.termsOfServiceUrl("http://blog.csdn.net/saytime") .version("1.0") .build(); } }
Controller
@Api(tags="Employee Part") @RestController public class EmployeeController { @Autowired private EmployeeService employeeService; // ============================ CRUD =========================== @ApiOperation(value="List Emlpoyees") @ApiImplicitParams({ @ApiImplicitParam(name="page",value="页码",dataType="Integer",paramType="query",required=false), @ApiImplicitParam(name="size",value="每页数量",dataType="Integer",paramType="query",required=false) }) @GetMapping("/employees") public Object listEmployees(@RequestParam(name="page",required=false) Integer page ,@RequestParam(name="size",required=false) Integer size) { if(page!=null && size!=null) { return ResponseUtil.ok(employeeService.listByPage(page, size)); }else { // return new ResponseEntity<>(employeeService.listAll(),HttpStatus.OK); return ResponseUtil.ok(employeeService.listAll()); } } @ApiOperation("Get Employee By Id") @ApiImplicitParam(name="id",value="Employee唯一标识",dataType="Integer",paramType="path",required=true) @GetMapping("/employees/{id}") public Object getEmployee(@PathVariable Integer id) { return ResponseUtil.ok(employeeService.getEmployee(id)); } @ApiOperation("Update Employee By Id") @ApiImplicitParam(name="id",value="Employee唯一标识",dataType="Integer",paramType="path",required=true) @PutMapping("/employees/{id}") public Object updateEmployee(@PathVariable("id") Integer id,@RequestBody Employee emp) { emp.setId(id); Integer result=this.employeeService.updateEmployee(emp); return ResponseUtil.result(result==1, result); } @ApiOperation("Add Employee") @PostMapping("/employees") public Object insertEmployee(@RequestBody Employee emp) { emp.setId(null); Integer result = this.employeeService.insertEmployee(emp); return ResponseUtil.result(result!=null, result); } @ApiOperation("Delete Employee By Id") @ApiImplicitParam(name="id",value="Employee唯一标识",dataType="Integer",paramType="path",required=true) @DeleteMapping("/employees/{id}") public Object deleteEmployee(@PathVariable Integer id) { Integer result = this.employeeService.deleteEmployee(id); return ResponseUtil.result(result==1, result); } }
entity
@ApiModel("Employee Model") @Data public class Employee { @ApiModelProperty("唯一标识") private Integer id; @ApiModelProperty("名称") private String name; @ApiModelProperty("备注") private String remark; @ApiModelProperty(name="部门Id",notes="Department table primary key") private Integer departmentId; }
run then visit: http://localhost:8080/demo/swagger-ui.html