天空Blond - 安卓
type: channel
link: https://www.skyblond.info/tag/%E5%AE%89%E5%8D%93/
items: 【代码札记】在安卓中实现骑行时多人聊天
items: 【代码札记】从零开始的安卓应用开发 - P4
items: 【代码札记】从零开始的安卓应用开发 - P3
items: 【代码札记】从零开始的安卓应用开发 - P2
items: 【代码札记】从零开始的安卓应用开发 - P1
items: 【代码札记】从零开始的安卓应用开发 - P0

【代码札记】在安卓中实现骑行时多人聊天
type: item
date: 2023-09-05T14:34:12+08:00
description: 上一篇文章介绍了我这个语音聊天的协议设计。本文将介绍我是用安卓实现APP的过程。前言由于我不是专业的安卓开发，所以本文写的一些内容可能不是最佳实践，甚至可能与之背道而驰，只是姑且能用的草台班子罢了。如果你发现了这样的问题，欢迎在评论区中友善地提出来，我会十分感谢你的友好纠正。此外本文的写作意图不是从头到尾的记录或手把手的教学，而是记录我在开发过程中遇到的问题，以供未来我或其他人参考之用。如果有部分描述得不够详细，属于正常范畴。第一个Activity新建项目后会自带一个主Activity，作为一个手生的懒人，我自然想着页面总数越少越好、每个页面越简单越好。所以没有那些花哨的广告和宣传，还有什么存钱贷款之类的功能，我的APP就要做一个小而美。在程序成型之前，这个主页面被用来开发和测试新功能。在开发时我并不知道这个APP到底能不能成功，所以在主界面开发基本功能，测得差不多了再把他们打包封装成其他Activity。程序开发的差不多之后，主页面就作为设置页面，用来启动和关闭相应的功能。而另一个详情页面则用于展示广播中的其他节点和调整不同节点的音量。整个app一共两个Activity和一个前台服务，整体APK打包之后不到5MB。朋友们，这才是小而美啊！作为设置的页面，当然越简单越好。因为这个页面要管理服务，所以需要让用户进行一些配置：用户的昵称，用来向其他节点宣告加密用的密码，用来防止未经授权的监听组播信息，用于决定最终组播使用的ip和端口号关于组播IP，上一篇文章已经决定了，我们要使用239.255.0.0/16。但是用户不一定记得住，说实话我也记不住，这个是从上一篇文章复制粘贴来的。怎么办呢？训练用户背下来就好了。我们当然可以把IP地址分成四段，前两段是固定的label，后两个是文本框，但这样一来我就需要两个文本输入来决定IP，太麻烦了。如果往根上说，IPv4地址就是一个32bit的数据，那对应的第16位就是我们这里要由用户决定的。16bit能搞一个什么数据类型呢？当然是short了。所以我们就搞一个文本框，让用户输入一个0到65535之间的数字，我们用这个数字和前面的239.255一拼接，这不就决定好ip了？这个懒偷得很妙，我是说，这个设计很简洁、巧妙。当然，如果你再想想看，端口号的取值范围也是0到65535，两个16bit拼在一起就是一个int了，不如让用户输入一个整数，这样一来我们一个文本框就解决了两个参数，岂不是更妙？我确实考虑过这种设计，抛开数值过大，会给用户交互带来困扰不说，安卓本身也是个unix系统，所以1024以下的端口只有系统能够占用，我又不是拼多多，作为一个普通程序不能也不应该去使用root权限，所以端口号的范围就限制在了1024以上的端口。再说说看用户交互的问题。你让用户随机挑一个0到65535之间的数字，我觉得这个分布还是挺随机的；但如果你让用户在0到42亿之间随便挑一个数，你能指望用户给你输入10位的数字？更何况用户要避免这个数字被解析出小于1024的端口号，简直是难上加难。如果这样设计的话，恐怕用户的输入都要挤在6位或8位数字以内，而我的本意是希望所有的地址和端口都能够平等地被用户随机选择。所以很遗憾，懒偷得多了就要变成烂活，我选择单独搞一个输入端口号的文本框。密码和用户昵称这个就彻底没办法偷懒了，只能老老实实地弄两个文本框。最后再搞三个按钮，分别用于启停服务和切换到详情页。是的，启动和停止我使用了两个按钮。我知道可以使用bind拿到服务，然后看启停状态，把启动和停止做到一个按钮上。但这不是懒嘛。换个角度想，我这种设计避免了页面整体被recompose的次数，四舍五入算省电设计。持久化保存配置当我测试完配置页面的逻辑之后，我发现了一个问题：我每次关掉app重新打开，就得把昵称、密码那些东西重新填写一遍，我要是用户，这个app肯定活不到它第三次启动。为了解决这个问题，我搜索了一下，安卓说要用preferences这个东西。简单地说，它提供了一个十分简单但又够用的datastore，允许程序员快速存储配置。关于它的使用方法我就不多说了，关键词都给你列出来了，想知道就自己去谷歌啊。在我的APP中，我使用了一种简单但又没那么粗暴的方式来使用它：通过订阅flow来实时更新对应的文本框，在启动服务时异步地将新配置保存回去。最开始我直接把文本框的内容和配置项绑定到了一起，导致文本项中的一个变化要传递到它的值上，然后这个值再被保存到配置项里，然后订阅的协程收到变化后再把变化的值赋给文本框，这一个流程下来会增大处理用户输入的延迟，并且这其中好多都是无用功。所以最终没有使用这种方案。前台服务服务保活可能是所有开发者最头疼的事情了。因为不同的厂家会在自己的ROM里实现不同的后台管理。有些厂商为了满足自己“省电”的宣传，可能杀后台…

【代码札记】从零开始的安卓应用开发 - P4
type: item
date: 2023-05-02T15:31:00+08:00
description: 哈哈，没想到吧，竟然还有续集！其实我也没想到。本文介绍我如何为我的安卓APP追加扫码功能。背景本来上一篇就完结了，APP也写完了，目前也没有需要追加的功能了。但是在实际使用中发现，使用Google的条码扫描器，经常会出现Google Play Service自废武功的情况。为了解决这个情况我真是新买了一台三星S23+，但情况还是没有改善。这部分的故事是这样的。我原本使用的是红米K40 Pro，使用了MIUI EU版本的系统，这个是一群票友基于中国版MIUI固件，缝合了谷歌全家桶，并删减了一些臃肿的小米框架得到的第三方ROM。在此基础上我还进行了Root，并安装了一些Magisk模块，例如存储空间隔离等。总之是一个非常客制化的一个环境。在进行长期测试时我发现Google的条码扫描服务经常在一段时间之后变得不可用。具体表现就是抛出com.google.mlkit.common.MlKitException: Failed to scan code这样的异常。经过一番搜索，我发现清空Google Play Service的数据能够缓解这个问题——因为扫描服务是按需下载的，清空数据之后要重新下载扫描服务，当然就可以恢复正常。但是过一段时间（一天）又会抛出异常，还得再清空，就很烦。于是我开始搜索详细原因。在Google官方的一个Issue tracker上我看到有一个人说关于Google Play Service版本的问题，我查了一下，我这个好像不是最新版，于是就想办法升级。但是升级过程中我发现Play商店总是失败——并不是那么明显的失败，而是点了升级按钮之后，短暂读条，然后立刻又变回去了。我尝试过单独下载最新版的Google Play Service的APK，手动安装，但是系统提示我签名不一样。我寻思，这不对啊，都是谷歌的APK，怎么会签名不对呢？于是我又回到Google Play商店开始点升级按钮。按照以往的经验，有时候Play商店会犯傻，因为在国内需要使用代理访问，所以Play商店会误判无网络，从而导致安装失败。于是我开始不停的点升级按钮，然后到了某一刻，开始读条之后没有退出，我以为是Play商店想明白了。过了一段时间（指看了一会儿YouTube），我也没有检查商店页面有没有升级好（默认情况下在Play商店是搜不到Google Play Service的），就直接回到我的APP调用了扫码服务。结果这一下手机就卡死了，因为我用的是全面屏手势导航，所以界面也不响应我的手势。我想，那重启一下应该就没问题了吧。结果这一个电源键按下去，它就再也不启动了。具体表现是就是开始会过logo动画，过完动画之后就卡死在这里了，不进入桌面。但是指纹传感器会有震动反馈。然而屏幕不响应触控操作，重启之后需要键盘才能解锁。理论上我可以尝试插入一个usb键盘，但我觉得希望不大。所以就直接刷机了。四清了四五次，这手机才重新见到桌面。关于这个过程，我猜测有如下几种可能：票友缝合的谷歌框架有问题，因为刷机之后我查询Google Play Service的安装时间，它反馈是2009年，相比之下，三星系统的安装时间显示的是2023年；与此同时，我从网络上下载的APK提示签名不符，这其中肯定是有修改系统在Root之后影响了Play商店的更新，导致更新时把系统搞坏了我安装的Magisk模块干扰了Play商店的更新，但具体是什么模块不清楚总之我决定不再使用票友制作的ROM了。一方面是小米的系统太臃肿了，基于中国版ROM，但又缺乏中国版ROM的特性（例如公交卡、钱包等），有Google框架，但没有认证级别（原版MIUI据说有，但是很明显，经过EU修改之后就没了）。总之就是不如原生的体验。所以左思右想，换了三星S23+，关于这个设备，在我使用一段时间之后将会写文章和大家分享感受。API设计总之，既然没有办法绕开这个Bug，就只能自己实现扫码功能了。在开始之前，我们先看看谷歌这个库的API设计：val scanner = GmsBarcodeScanning.getClient( this, GmsBarcodeScannerOptions.Builder() .setBarcodeFormats( Barcode.FORMAT_DATA_MATRIX, Barcode.FORMAT_CODE_128 ).build() ) scanner.startScan() .addOnSuccessListener { barcode -> // use the barcode } .addOnCanceledListener { showToast("Scan canceled") } .addOnFailureListener { e -> showToast("Scan failed…

【代码札记】从零开始的安卓应用开发 - P3
type: item
date: 2023-05-01T14:58:00+08:00
description: 本文介绍APP的UI部分。前面铺垫了那么多文章，终于到UI部分了。Material 3在开始之前，我想先说说看Google的这个设计语言。因为我也不是专业搞前端的，所以在这方面可能说的有些错误，一切以实际为准。简单地说Material 3提供了一系列风格统一的UI组件，并针对颜色、字体、形状等要素提供了较为一致的指导。然而在使用Android studio创建APP时，默认引用的是Material 2，如果想使用Material 3的话，好像还需要折腾一下依赖这些东西。关于Material 3的优点，我认为最大的就是他们的色彩系统。官方有一个调色器，可以根据你上传的壁纸自动生成一系列看起来很融洽的色彩，比我手动挑的好看多了。除了这一点之外，在安卓13上还有系统级别的动态配色：也就是说你的APP可以根据用户设定的壁纸，自动采用同样颜色的配色，使得整个系统看起来更加统一（我想特立独行的流氓APP，哦不是，我是说国产APP，他们应该不会采纳这样的方法）。我这里就使用了官方的调色器生成了一套主题，但看起来好像并没有使用动态主题，所以还需要手动改一下：@Composable fun VazanTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val useDynamicColors = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val colors = when { useDynamicColors && darkTheme -> dynamicDarkColorScheme(LocalContext.current) useDynamicColors && !darkTheme -> dynamicLightColorScheme(LocalContext.current) darkTheme -> DarkColors else -> LightColors } MaterialTheme( colorScheme = colors, shapes = Shapes, typography = Typography, content = content ) }由于动态配色是安卓13才有的，所以需要判断一下。这里我们的逻辑是：如果有动态配色则优先使用动态配色没有则使用App自己的配色至于字体、形状这些，我并没有什么自定义的，就自带的挺好。权限然后再说说看权限。有些权限不需要向用户申请，比如说联网权限。但是有些就需要申请，而用户有可能会拒绝。虽然谷歌建议APP应该允许用户拒绝授予权限，并在这种情况下尽力提供服务，但是对于这个APP来说，像是蓝牙、相机这种对于功能来说很必须的权限，拒绝了就没法使用了。例如蓝牙权限，既然要通过蓝牙调用打印机打印标签，你不给我蓝牙权限，我怎么和打印机通讯呢？基于这种观察，我设计了一种以Activity为单位的权限申请机制。总地来说权限在进入需要权限的Activity之后才申请，并且申请失败直接退出这个Activity。具体做法就是继承ComponentActivity，在里面实现申请权限的逻辑，然后再由其他Activity继承这个抽象的Activity：@AndroidEntryPoint abstract class VazanActivity : ComponentActivity() { protected abstract val permissionExplanation: Map<String, String> private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> permissions.forEach { (permission, isGranted) -> if (!isGranted) { AlertDialog.Builder(this) .setTitle("Failed to grant permission") .setMessage( "Permission: $permission is not granted.\n" + "This permission is required for ${permissionExplanation[permission]}." ) .setCancelable(false) .setNeutralButton("Fine") { dialog: Dial…

【代码札记】从零开始的安卓应用开发 - P2
type: item
date: 2023-04-28T17:07:00+08:00
description: 本文来介绍安卓APP的依赖注入，以及APP中一些机制的设计。依赖注入作为一个Java后端程序员，依赖注入这个概念肯定是不陌生：Spring框架这种理科男设计出来的玩意儿，真是要命。文档里没有几句人话，全是黑话和暗语，AOP，依赖注入，IOC，OOP，动态代理，CGLIB……只有他们能懂。正常人不可能看懂。想用旧代码加上个面向切面编程——看看，全是这种黑话，切面还编程。这是人话吗？弄了半天，一头大汗，也没弄明白。网上所谓的教程，是同一帮人写的，用同一套黑话。不弄了……credit: https://disksing.com/sao-gen-gen/43PS：在中国北方，切面是面条的一种。所以用面条编程，还是挺搞笑的如果读者不了解依赖注入的话，这里简单的举一个例子。按照面向对象的原则，我们在编程的时候希望把不同功能的代码放到不同的类里面。以汽车为例，Car需要有引擎，有电池，有车载电脑，有控制系统，而这些细节的实现不应该放在Car类里面，而是单独作为各自的类，通过接口来实现功能的调用。例如电池接口只需要供电和充电，而不必让调用者考虑这电池用的是什么材料，使用了什么技术。也许电池比较简单，那车的控制系统呢？一个控制系统会有好多输入输出，而这些输入输出也是不同的模块。这样一来，你要实例化一个汽车对象，就要先实例化它依赖的对象，而它依赖的对象又要依赖其他对象。日后如果你要对某个子系统新增一个依赖，那只能祝你好运了。而依赖注入就是来解决这个问题的。你只需要告诉依赖注入的框架如何制作这些基本的元素，而后依赖注入的框架就可以自动地根据需求来“装配”依赖，进而实例化对象。依赖注入经常和反转控制（IOC）一起说，因为在以前，是我们程序员来编写代码，控制不同组件实例化的时机，进而用实例化的结果来做事。引入了依赖注入之后，实例化组件的时机不再受我们的控制，反而是我们的代码将在不确定的时机被框架调用，这就是反转控制大体思想。当然，作为不使用依赖注入的反转控制，你也可以做一个全局共享的类，然后将所有需要的零部件放到这个类里面，通过静态成员按需使用。但这种情况下你还是需要维护一个类似于查找表的东西，我觉得这种手动的东西不如自动的框架来得优雅、简洁。对于安卓上的依赖注入框架，我喜欢叫他Dagger hilt，虽然dagger和hilt是两个分开的框架。关于这些库的历史渊源我就不讲了，谷歌上都可以查到。简单地说Dagger是一个谷歌维护的（官方已经钦定了）安卓上的依赖注入框架。它实现了JSR 330（Dependency Injection for Java），并且主要利用javax.inject.Inject注解来对非私有成员和构造器进行注入。Dagger是在编译时期确定依赖关系的，因此在编译时Dagger会根据你提供的对象和需要被构造的对象生成Factory类的代码，从而实现依赖注入。而Hilt则是一个使用了Dagger，并意在简化Dagger使用的库。在安卓上，一些特定的类是不能直接进行依赖注入的，例如安卓的Activity。要对这些类进行依赖注入，通常需要使用Dagger生成的工厂类。虽然Dagger帮我们生成了这些工厂类，但最终让安卓使用这些工厂类的代码还是要我们来写。而Hilt就是解决了这个问题，只需要一个注解，我们就可以直接享受到完整的依赖注入——不再需要额外的配置代码了。要开始依赖注入，我们首先需要提供一些可供注入的素材。这里为了简单考虑，所有的东西都作为单例模式注入。也就是说，这些对象一经创建，就要跟着整个APP的生命周期走。@Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideAppDatabase(app: Application): AppDatabase = Room.databaseBuilder(app, AppDatabase::class.java, "vazan").build() @Provides @Singleton fun provideConfigDao(database: AppDatabase): ConfigDao = database.configDao }这里我们就提供了两个对象。一个是AppDatabase用来访问数据库的；另一个是从数据库里摘出来的DAO。至于最开始的那个Application从哪搞，这也好办：@HiltAndroidApp class VazanApplication : Application()直接创建一个就好了。当然，Provide并不是提供对象的唯一方式。Provide提供了一种教框架构造对象的方法。但有时候只是把参数传来传去，很没意思。…

【代码札记】从零开始的安卓应用开发 - P1
type: item
date: 2023-04-26T15:31:00+08:00
description: 本文来介绍APP的整体架构和数据源设计。前文简单描述了这个APP的思路来源和潜在的应用场景，本文首先介绍一下APP的技术和架构。技术和架构众所周知，我很讨厌在UI中使用XML来定义布局。我不止一次自己写了一个简单的声明式框架来帮助我渲染UI，这其中就包括我的软件课设和毕业设计。所以同样地，我也讨厌在安卓中使用XML文件来定义布局。好在Google整出来了一个Jetpack Compose，它基于Kotlin，允许开发者使用声明式的方式定义和渲染布局，并且整个生态支持的相对还不错，在我看在，大部分情况下要比原来的UI更加便于使用。关于数据库和其他框架部分，几乎都支持Kotlin协程，而不用依赖额外的什么RxJava这些。在我看来甚是得体和优雅。总体来说，APP的界面部分使用Compose来实现，数据库使用Room框架，HTTP请求则使用Retrofit2框架，外观使用Material 3，依赖注入部分使用dagger hilt。对于我的第一个安卓APP来说，这看起来还挺中规中矩的是吧。关于应用程序的架构，好像我并没有遵循一个特定的准则。我更倾向于凭直觉分割代码。例如数据源的部分分成来自Room数据库的和来自API的；基于这些数据源，针对应用的功能提供类似于Repository、用于提供交互的接口（即业务逻辑不考虑这些数据的原始形式）；然后是业务逻辑，通常不考虑UI部分，例如生成条码等；最后是UI部分。我也不知道我这种组织方式叫什么，反正写代码的是我，我自己开心就好。数据源设计接下来我们说说数据源的设计。数据源分两种：数据库和Memento API。Memento在正式开始之前，我们先说一说Memento那边需要怎么建立Library。我打算分为三个Library：位置、容器和物品。位置部分比较简单，唯一的要求就是具有一个能够区分不同位置的字段。这个字段在Quick Scan操作（指定目的地，通过扫码来向API发出请求，实现将被扫描的物品移动到目标位置）中会被显示给用户，让用户挑选一个位置作为目的地。容器则相对麻烦一些：容器可能在一个位置下（即位置与容器是1对N），也可能在其他容器里（容器与容器1对N），但这个容器不可能既在一个位置，又在另一个容器里，遗憾的是Memento数据库没办法提供这种检查，但索性我们可以通过编程来修改（设置字段的时候将另一个字段设置为null）。同时容器还要求一个条码字段。物品与容器类似，但这次条码字段不是必填的，因为考虑到有些物品太小了，没必要贴条码（真的不会有人给每一根数据线都贴上条码吧，不会吧不会吧不会吧）。除了上述必须的字段，剩余字段可以自由发挥。例如在物品这里可以添加一个勾选框，用来标识这个物品有没有内含电池，这样收纳的时候就可以针对有电池的物品特殊照顾。而这些字段都是和APP不相关的，不会干扰APP的运行。数据库首先是数据库设计。由于核心数据都存储在Memento那边，所以APP这块只要记录最基础的数据就好了。实体首先是负责记录配置的键值对，这个表比较通用，充当了一个KV存储：@Entity( tableName = "configs", primaryKeys = ["config_key"], indices = [Index("config_key", unique = true)] ) data class Config( @ColumnInfo(name = "config_key") val key: String, @ColumnInfo(name = "config_value") val value: String, )在使用时，键的部分通常以scope.namespace.something的形式来表示。例如有关APP中记录最后一次使用的打印机地址可以表示为app.thermal_printer.last_address，而用于访问memento API的密钥则表示为memento.api_key，类似地，记录Memento中存储位置的库的ID可以表示为memento.location.library_id，大概就是这种感觉。值的部分则使用String表示，由具体的设置自行决定。然后是记录标签的表。在打印的时候我们需要获知正在使用的标签（来自Memento API的数据），同时也要记录我们曾经打印过但还没有在Memento中使用的标签（由我们加入到数据库中）。另外在Quick Scan操作中，我们需要Entity ID来编辑被移动物品的位置，即通过扫描物品的标签，我们需要记录标签到Entity ID的映射。同时基于标签的编码形式，我们不需要区分这个标签是容器标签还是物品标签，因为容器标签总是以字母B开头，而物品标签总是以字母I开头。数据库设计如下：@Entity( tabl…

【代码札记】从零开始的安卓应用开发 - P0
type: item
date: 2023-04-24T14:50:39+08:00
description: 虽然我以前也尝试过开发安卓APP，但最后都没能成功。好在这一次成功了，姑且就算它是第一次吧。我想写一个个人物品管理的APP。事情的起因源自于我时常找不到曾经买过的东西——这并非我丢三落四，而是我每年都要搬迁几次，每次搬迁都要把自己的东西打包到箱子里。然而安定下来之后，我不会一下子都把他们拿出来，如果没有用到的，那就留在箱子里，这样一来下次搬迁就不用动了。但是几次搬迁累积下来，基本上我也不知道什么东西在哪个箱子里了，尤其是之前没用到的。所以每次找东西都得挨个箱子翻找，搞得一团乱，有时候找到一般就很急躁地不想再找了。有一天我接快递的时候看到快递盒子上面贴的面单，这无意间提醒我了：我可以在箱子上贴一个二维码，然后扫一下不就知道里面是什么了。所以我在春节期间糊了一个APP，也就是本文所介绍的APP的第一个版本。这个版本使用了我之前折腾过的喵喵机P2，通过蓝牙调用打印机在热敏标签纸上打印一个二维码，这个二维码是一个UUID，每个盒子对应这么一个UUID，然后APP提供了简单的日志功能，扫描一个UUID就可以针对这个UUID按时间顺序添加备注。这个备注相对比较自由，你可以写放进去了什么东西，里面有什么，拿出来了什么，把箱子搬到了什么地方之类的。找东西的时候直接全文搜索，找到与关键词相关的备注，继而找到与备注相关的箱子。箱子上的标签除了二维码，还有文字版的UUID方便寻找。总第来说，第一版APP构造十分简单，易用性不高，但足够使用。第一版APP是我一边看文档一边凭感觉写的，使用了Jetpack Compose，没有XML的日子是真的爽啊！第一版的APP在代码上可谓是非常简陋了：只有一个数据库实体，一些抽象出来的类，剩下的页面逻辑就全都放在Activity里面了。那个时候的我也不知道什么MVC、MVVM这些东西，反正就硬写，能用就行。但话是这么说，虽然它在功能上满足了一个最小全功能集，但是这就好比，Java能做的事情汇编也能做，但没有人会用汇编来写复杂的业务。这个第一版也是一样。假如说你把一个物品从A箱子移动到B箱子，你就得先扫A箱子，记上我把东西拿出来了，或者删掉这个东西的备注，然后再扫B箱子，记上我把这个东西放进去了。如果期间疏忽大意了，很可能这个东西就在系统里找不到了，或者在系统里同时出现在多个箱子里，归根到底，还是没有彻底解决问题。我对于这个APP并不满意。首先它不够实用，其次我好像也没学到什么，就是花费了时间和精力编写了一个赛博垃圾。于是我开始寻找现有的解决方案。大部分我见到的解决方案都是针对商用场景的，比如IT资产管理或者供应链、库存管理等。对于我的需求太说，这就像是开宝马去菜市场买菜，实在是大可不必。后来我找到了两个相对合适的APP。第一个是HouseBook，看起来这个APP是由一个独立开发者编写的。这个APP给我带来了不少启发，例如多个实体类型：房屋、位置、容器、物品，其中房屋是最顶级的单位，一个房屋可以有多个位置，一个位置下面可以有容器或物品，而一个容器里也可以有子容器。同时它也提供了一种比较方面的交互界面：容器和物品可以与二维码绑定，容器可以有名字，物品可以有固定字段，例如品牌、型号、序列号、保质期等。在搜索方面也比较方便：可以使用文字搜索相关的容器或物品，也可以直接扫描二维码搜索对应的实体。我虽然很喜欢它的灵活性，但在物品字段方面我认为有些死板了：你只能使用应用中定死的，而不能自己新增字段。同时这个APP是付费的，需要一次性支付30USD来解锁全部功能，同时使用云同步的话，随着物品数量的增多，你可能还需要额外花钱购买云空间。不过这些都是一次性消费，没有持续订阅。第二个APP严格来说不算是物品管理，而是一个通用的个人数据库，叫做Memento Database，背后是一个商业公司在支持和维护（至少看起来像是，也许这个公司就只有开发者一个人也说不准）。同时这个APP支持安卓、iOS和桌面版，其中桌面版是Java写的，因此支持全平台。不过要跨平台使用，就需要订阅他们的高级计划。如果只是在一台设备上使用的话，那么不订阅也是可以享受到大部分功能的。这个App以库（Library）来组织数据，以数据库的语言来说，这个库其实就是一个表。你可以在表中定义字段，但是不需要像关系型数据库那么严格——你的表甚至可以没有唯一字段（因为每一个记录有一个内部的entity id）。同时在字段类型的选择上也非常丰富：字符串、整数、小数这些就不说了，什么布尔啊货币啊时间啊，这些都比较常见。他们还提供了条码字段，可以直接扫描条码来读取其中的信息，同时还提供了列表、树、标签、外键、自定义计算（基于其他字段计算出结果），如果这些还不能满足你的话，他们还支持JavaScript脚本。可以说是非常强大了。后来经过一番探索，他们还提供了一个云API用来操作开启云…