Tetris Native揭秘|有道词典动态化运营引擎

Tetris Native是有道词典端侧动态渲染引擎,目前已作为多个业务的运营投放容器,支持跨端UI动态化发布及多种样式,助力有道词典流量变现。《Tetris Native揭秘》系列文章将详细介绍Tetris Native的设计理念和详细落地方案。

[Read More]

Flutter入门1——Dart语言基础

Dart语言的某些特性可能会让习惯使用Java或者Kotlin的开发者看不懂或者感到疑惑,本文主要介绍Dart语言的一些和Java以及Kotlin不太一样的地方,旨在让Android开发者可以快速掌握Dart语言。

[Read More]

深入理解Android Runtime

Android Platform Architecture

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介绍应用Profile-Guided Optimization(PGO)技术对应用启动速度进行优化的可行性。

[Read More]

TargetsdkVersion 升级31(Android12)适配

TargetsdkVersion 升级31(Android12)适配

我们升级到Targetsdk29有大半年时间了,今年为了满足审查去除蓝牙的精确定位权限,以及满足上架Google Play的要求,需要将Targetsdkversion升级到31,适配到Android12。这个过程遇到不少坑,这里记录一下,希望能对大家有所帮助。

[Read More]

Kotlin升级1.5版本synthetic引发的血案分析

场景重现

因为项目里面Kotlin版本还停留在1.4,看到1.5版本更新记录提升了性能并且新加了一些特性,准备怒升级一波。怀着开心的心情升级完之后,运行起来就傻眼了!

1625820518128283

视频列表有个浮层没有隐藏,就升级下Kotlin,居然还有这个问题,真是太不可思议了!把Kotlin降级回去,然后就好了,确定是因为Kotlin升级导致的问题。接下来就开始分析了。

[Read More]

Android深色模式适配原理分析

Dark Mode

背景

从Android10(API 29)开始,在原有的主题适配的基础上,Google开始提供了Force Dark机制,在系统底层直接对颜色和图片进行转换处理,原生支持深色模式。到目前为止,我们从用户数据分析**50%**以上的用户已经使用上了Android10系统。深色模式可以节省电量、改善弱势及强光敏感用户的可视性,并能在环境亮度较暗的时候保护视力,更是夜间活跃用户的强烈需求。对深色模式的适配有利于提升用户口碑。 转载请注明来源「申国骏」

[Read More]

Flutter学习笔记——用户界面

以下为对Flutter官网的学习总结,如果你想快速掌握知识点,或者想复习一下官网学习的内容,那么值得看看。 转载请注明出处:Lawrence_Shen

用户界面

widgets介绍

  • Flutter一切都是widget,包括设置padding的container。
  • 几乎所有widget都通过build方法声明其UI
  • StatelessWidget用于固定样式的widget,StatefulWidget用于根据数据变化的widget。
  • StatefulWidget通过createState关联私有的State对象,并通过setState()方法更新数据并通知UI变化。
  • 更新UI时Flutter会通过比较前后widget树来计算差异,widget只是保存了样式信息,它的重建可以考虑是轻量级的。widget树会对应到element树,并通过element树创建Render树。相同类型widget会重用element和render对象。
  • State对象的生命周期跨越其对应的widget对象build方法,比widget本身生命周期要长
  • State调用流程大致为initState -> build -> dispose,可以在initState做初始化操作,在dispose中做清理操作
  • didChangeDependencies会在initState和build之间调用,当父widget有InheritedWidget变化时也会被调用
  • InheritedWidget可用于在widget树中给子widget共享数据,通常通过of方法调用context.inheritFromWidgetOfExactType返回拥有共享数据的InheritedWidget对象
  • key控制widget重建时与哪些其他widget进行匹配,从而保持正确的state状态,一般用在widget的添加删除或者重排序中控制widget重用
  • key分为Local key(value key表示根据某个值判断、Object key表示根据某个对象判断、Unique key表示每个widget都不一样) 和Global key(表示不同页面的widget共享),

构建layouts

Flutter中的layouts

  • 可以通过Row和Column构建复杂页面
  • mainAxisAlignment控制主轴对齐方式,crossAxisAlignment控制次轴对齐方式
  • 使用Expanded widget来fit window,flex来指定比例
  • 将布局widget赋值给变量,通过变量组合布局减少层级嵌套
  • 使用Container设置margin、border、pandding和背景
  • GridView.extend中maxCrossAxisExtent设置每个item的最大宽度,mainAxisSpacing设置主轴item之间的间隔,crossAxisSpacing设置次轴item之间的间隔,childAspectRatio设置item宽高比例
  • GridView.builder用于数量较多的item展示,仅加载当前可见的部分,GridView.count用于加载少量固定数目的item并指定每行item格式,GridView.extend用于加载少量固定item并指定每行item最大宽度
  • GridView中通过SliverGridDelegate控制子widget如何布局,通过SliverChildDelegate来获取子widget,可以通过自定义Delegate来实现自由或者叠加布局。
  • GridView和ListView都继承自BoxScrollView
  • 大量数据需使用ListView.builder并在itemBuilder回调中创建并提供widget;如果列表的item样式可以提前构建则可以直接使用new ListView;ListView.separated除了itemBuilder之外还有个separatorBuilder用来定义分隔线样式;ListView.custom通过提供自定义的SliverChildDelegate来实现自定义的列表加载和缓存逻辑。
  • Stack用于widget的堆叠,可以做渐变的图片阴影
  • Card内部内容不能够滚动,可以自定义圆角和阴影大小
  • ListTitle是方便构建至多三行文字加上前后图标的列表item widget

layout使用例子

  • 使用Expanded widget占满剩余空间,子widget设置CrossAxisAlignment.start表示从前开始
  • Text softwrap控制是否需要自动换行
  • 修改pubspec.yaml设置assets目录,例如:flutter: assets: [images/]
  • Image.asset中设置fit:BoxFit.cover 表示图片应该以最小的大小占满box空间
  • 使用ListView代替Column保证小屏幕手机中空间可以滚动

创建自适应UI应用

  • 使用LayoutBuilder的BoxConstraints获取当前widget的宽高比例从而调整子widget布局
  • 使用MediaQuery.of()获取屏幕宽高和旋转方向等设备信息从而控制整体布局样式
  • AspectRatio控制子widget的宽高比例
  • CustomSingleChildLayout、CustomMultiChildLayout将子widget的布局委托给ChildLayoutDelegate进行控制
  • FittedBox:当子widget比父widget大时,通过FittedBox可以设置子widget的缩放方式
  • FractionallySizedBox可以设置子widget占据其空间的宽高百分比
  • MediaQueryData中padding指代周边有多少不能绘制的区域不计算被键盘等遮挡的区域,viewPadding指的是周边有多少不能被绘制的区域不受键盘等遮挡影响,viewInsert表示周边有多少区域被键盘等遮挡了
  • OrientationBuilder获取屏幕是否旋转

Constraints布局约束理解

  • 布局流程:
    1. widget从parent中获取四个约束,分别是最小和最大宽度、最小和最大高度;
    2. widget将约束一个一个地传递给子widget,并让子widget根据约束条件设定其自身的大小;
    3. widget根据子widget的大小一个一个进行布局;
    4. widget将自身的大小上报给parent。
  • 布局流程会导致以下三个限制:
    1. 一个widget最终布局大小需要受到parent的约束限制,不是想要什么大小都可以;
    2. 一个widget不能知道也不能决定其在屏幕中的位置,widget的布局由其parent决定;
    3. 只有考虑整棵widget树才能确定widget的大小和位置,不能准确地定义某个widget的位置和大小。
  • Container布局行为:
    1. 若没有子widget,没有设置宽高,没有约束,parent是无界约束,Container会填充parent,并希望让自身尽量的小
    2. 若没有子widget,没有设置alignment,设置了宽高或者有约束,Container会在满足自身约束和parent约束的情况下尽量的小
    3. 若没有子widget,没有设置宽高,没有约束,没有设置alignment,parent是有界约束,那么Container会尽量的扩大以满足parent的约束
    4. 若设置了alignment,parent无界约束,那么Container尽量缩小为子widget大小
    5. 若设置了alignment,parent有界约束,那么Container扩大为parent约束大小,并将子widget根据alignment设置来布局
    6. 若只有子widget,没有设置宽高,没有约束,没有alignment,Container会将parent的约束传递给子widget,并尽量缩小为子widget大小
  • 布局中FittedBox可以控制子widget在约束空间中的布局,例如设置自动缩小文字或者缩放图片
  • tight约束表示固定宽高约束,loose约束表示在设置最大宽高基础上尽量的缩小

Box constraints边界约束

  • 有三种box,分别是无限扩展例如Center或者ListView、子widget决定例如Trnasform和Opacity、固定大小例如Image和Text
  • 类似于当一个竖向的ListView嵌套进了一个横向的ListView,会造成无界约束状态(Unbounded constraints),这种状态会使得子widget可以在两个方向无限扩展导致错误
  • Flex boxs指的是Row和Column,表示当其处于一个有界的区域会不断扩展至给定大小,当其处于一个无界区域会适应他的子widget大小。
  • 如果将Flex box放置于类似于ListView的widget中,那么flex box中不能有类似于Expanded的widget,这会导致类似于Expanded的widget无限扩大造成错误
  • Column的宽度和Row的高度不能设置为无界的,否则他们的子widget将无法布局

加入互动逻辑

  • 可交互的widget有三点,一是有两个类,分别继承StatefulWidget和State,二是State类中拥有可变的状态和build方法,三是当状态变化,调用setState()方法对widget进行重绘。
  • 将Text放在SizedBox中可以防止当文字变化时由于宽度变化带来的位置抖动
  • 当调用setState({})方法时,会先执行lambda逻辑,然后调用_element.markNeedsBuild()标记当前element为dirty状态并在下一帧根据修改后的状态进行重绘
  • 有三种常见的管理状态方法,分别是:widget自己管理自己的状态、父widget管理状态、混合前两种方式
  • 如果状态是用户数据,那么最好在父widget管理。如果状态是与界面效果有关的例如动画,那么最好在widget自身内管理状态。如果不确定最好先在父widget中管理,因为大多数情况外层需要对状态数据进行处理并更新子widget,外层处理状态也有利于子widget保持整洁。当widget既包含用户状态又包含外部不关注的自身界面效果状态则使用混合状态管理模式。
  • 对于必须传入的参数使用@require注解

添加assets和图片

  • 在pubspec.yaml中声明assets文件夹的路径声明,如果需要添加子文件夹的话需要单独列出
  • 声明assets时会同时查找其定义的子文件夹是否有同名的文件,如果有的话会把同名的文件同时引入,这是为了方便引入不同分辨率的图片资源
  • 使用DefaultAssetBundle.of(context).load()或loadString()方法加载asset文本资源,其中context最好使用当前widget的BuildContext,这有利于父widget在测试或者本地化时在运行时替换不同的AssetBundle。
  • 当不能获取widget context的地方,可以使用rootBundle来加载文本资源
  • 对于图片资源,可以使用相同的图片命名并放在2.0x和3.0x文件夹中,不同dp/px比例的手机会自动选用合适大小的资源
  • 使用AssetImage加载图片会自动选择对应分辨率的图片,如果需要加载不同package的图片,需要在AssetImange中指定package
  • 对于不在同一个package的图片资源,也需要在pubspec.yaml文件中定义,例如需要引用package为fancy_backgrounds的图资源,需要在当前的pubspec.yaml中定义assets路径为packages/fancy_backgrounds/xxx(图片在fancy_backgrounds中libs目录下的相对位置)
  • 在Android中使用flutter的asset资源,使用PluginRegistry.Registrar.lookupKeyForAsset()方法获取key,并使用AssetManager.openFd(key)方法获取AssetFileDescriptor
  • 在iOS中使用flutter的asset资源,可以使用registrar lookupKeyForAsset或者key,然后通过mainBundle pathForResource:key ofType获取asset路径。如果使用了ios_platform_images插件,那么可以直接使用OC中的UIImage flutterImageWithName或者Swift中的UIImage.flutterImageNamed获取。
  • flutter中使用iOS的图片可以使用ios_platform_images插件中的IosPlatformImages.load方法
  • 启动页会在Flutter绘制第一帧的时候被替换,如果在main方法中不调用runApp方法,那么启动页将一直展示。
  • 加入启动页的方式需要使用Android和iOS的本身的方式加入。

页面导航

导航至新页面并返回

  • route在安卓中相当于Activity,在iOS中相当于ViewController,在Flutter中,route表示的只是一个widget
  • 页面导航的步骤:创建两个route,使用Navigator.push()导航到第二个route,使用Navigator.pop()返回到上一个route
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  )
  • 通过创建MaterialPageRoute适配安卓和iOS页面跳转的动效,通过设置maintainState释放上一个页面的内存,通过fullscreenDialog设置是否全屏dialog样式

使用具名路由跳转

  • 当页面之间跳转较多时,在MaterialApp中声明路由关系,然后使用具名路由导航Navigator.pushNamed()可以减少代码重复
  MaterialApp(
  // Start the app with the "/" named route. In this case, the app starts
  // on the FirstScreen widget.
  initialRoute: '/',
  routes: {
    // When navigating to the "/" route, build the FirstScreen widget.
    '/': (context) => FirstScreen(),
    // When navigating to the "/second" route, build the SecondScreen widget.
    '/second': (context) => SecondScreen(),
  },
);
Navigator.pushNamed(context, '/second');

非具名路由之间传递数据

  • 使用非具名路由跳转有两种页面间传递数据的做法,一种是跳转新页面时在Widget的构造函数中传入数据;第二种是通过设置MaterialPageRoute的RouteSettings中的arguments,并在跳转页面中使用ModalRoute.of(context).settings.arguments获取
  // 第一种方法
  Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => DetailScreen(todo: todos[index]),
        ),
    );
  // 第二种方法—设置参数
  Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => DetailScreen(),
            // Pass the arguments as part of the RouteSettings. The
            // DetailScreen reads the arguments from these settings.
            settings: RouteSettings(
                arguments: todos[index],
            ),
        ),
    );
  // 第二种方法—获取参数
  final Todo todo = ModalRoute.of(context).settings.arguments;

具名路由之间传递数据

  • 使用具名路由跳转有两种页面间传输的做法,一种是使用Navigator.pushNamed并设置arguments,然后在跳转页面使用ModalRoute.of(context).settings.arguments获取;第二种是是使用Navigator.pushNamed并设置arguments,然后在MaterialApp的onGenerateRoute方法中获取settings.arguments并在返回的MaterialPageRoute中通过构造函数设置给跳转页面
  // 第一种方法—设置
  Navigator.pushNamed(
      context,
      ExtractArgumentsScreen.routeName,
      arguments: ScreenArguments(
        'Extract Arguments Screen',
        'This message is extracted in the build method.',
      ),
    );
  // 第一种方法—获取
  final ScreenArguments args = ModalRoute.of(context).settings.arguments;
  // 第二种方法—通过onGenerateRoute方法构造目标页面并传递参数
  MaterialApp(
  // Provide a function to handle named routes. Use this function to
  // identify the named route being pushed, and create the correct
  // screen.
  onGenerateRoute: (settings) {
    // If you push the PassArguments route
    if (settings.name == PassArgumentsScreen.routeName) {
      // Cast the arguments to the correct type: ScreenArguments.
      final ScreenArguments args = settings.arguments;

      // Then, extract the required data from the arguments and
      // pass the data to the correct screen.
      return MaterialPageRoute(
        builder: (context) {
          return PassArgumentsScreen(
            title: args.title,
            message: args.message,
          );
        },
      );
    }
  },
);

从目标页面返回数据

  • 使用Navigator.pop设置数据,并使用await获取Navigator.push返回结果
// 设置数据
Navigator.pop(context, 'Yep!');
// 获取数据
final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => nextScreen()),
  );

动画

Implicit动画

  • 对于普通的修改大小和形状等的属性动画可以使用Implicit动画,设置动画时间duration、动画效果curve。常用的Implicit动画有以下这些:
    • Align -> AnimatedAlign
    • Container -> AnimatedContainer
    • DefaulTextStyle -> AnimatedDefaulTextStyle
    • Opacity -> AnimatedOpacity
    • Padding -> AnimatedPadding
    • PhysicalModel -> AnimatedPhysicalModel
    • Positioned -> AnimatedPositioned
    • PositionedDirectional -> AnimatedPositionedDirectional
    • Theme -> AnimatedThemeSize -> AnimatedSize
  • 若没有能满足需求的Implicit动画widget,那么可以尝试使用TweenAnimationBuilder来实现自定义属性动画

Explicit动画

  • 如果想要对动画进行播放控制,那么需要使用Explicit动画,并在turns中指定AnimationController。常用的Explicit动画有以下这些:
    • SizeTransition
    • FadeTransition
    • AlignTransition
    • ScaleTransition
    • SlideTransition
    • RotationTransition
    • PositionedTransition
    • DecoratedBoxTransition
    • DefaultTextStyleTransition
    • RelativePositionedTransition
    • StatusTransitionWidget
  • Explicit动画的几个概念:
    • Animaion<double>:CurvedAnimation和AnimationController都继承自Animaion<double>,通过Animaion可以获取动画的状态目前的插值,但是Animaion不会参与动画的绘制
    • CurvedAnimation用于定义动画的非线性过程;AnimationController用于控制动画播放进度,需要传入TickerProvider来减少处于屏幕外的动画资源消耗;Tween用于对Animation的范围进行转化;Animation可以通过设置Listners和StatusListeners来监听动画状态。
  • SingleTickerProviderStateMixin是TickerProvider的实现;mixin是线性叠加的代码继承,最后的类会覆盖前面类方法,mixin是类的一层一层叠加,类型判断可以为每一层的类,mixin更多强调的是代码的复用而不是类继承关系,mixin是一种类型不能实例化。参考:When to use mixins and when to use interfaces in Dart?

    Mixins is all about how a class does what it does, it’s inheriting and sharing concrete implementation. Interfaces is all about what a class is, it is the abstract signature and promises that the class must satisfy.

    [Read More]

Android性能分析&启动优化

两年前我做过了类似的启动优化分析《如何统计Android App启动时间》《如何优化Androd App启动速度》。两年过后,今天看来,之前说的nimbledroid工具已经需要收费,而且Android Studio自带的Android Profiler已经足够强大,并且Systrace也有了更为强大的Perfetto UI分析工具。我们是时候来重新学习一下目前性能分析的方法以及如何在分析的基础上做启动优化这个事情。转载请注明来源「Bug总柴」

[Read More]

单元测试之JUnit4

JUnit4

JUnit是一个帮助编写和执行单元测试的框架。可能很多人都接触过单元测试,但是只是停留在copy别人的测试代码再改一下的状态,下文尝试较为体系列举JUnit4中比较关键的一些知识点。转载请注明来源「Bug总柴」

[Read More]

Databinding subModule library 爬坑

问题描述

最近把gradle的‘com.android.tools.build:gradle’升级到3.2.1,升级之后dataBinding出错了,编译通过,但是运行时报了一个错误java.lang.ClassCastException: com.youdao.dict.databinding.FragmentYdliveBindingImpl cannot be cast to com.youdao.ydliveplayer.databinding.FragmentYdliveBinding。转载请注明来源「Bug总柴」

[Read More]