跳到主要内容

Jetpack Compose 搭建翻译APP

前言

FunnyTranslation 是基于Jetpack Compose写成的翻译软件。它首先是个可以用的软件,其次也是个开源项目。

开源地址:FunnySaltyFish/FunnyTranslation: 基于Jetpack Compose开发的翻译软件,支持多引擎、插件化~ | Jetpack Compose+MVVM+协程+Room

运行截图

图片图片
12
34

上面截图显示的所有UI都是基于Jetpack Compose搭建的,其他页面就不多截图了,防止占地方

软件介绍

本应用有以下特点:

  • Jetpack Compose + MVVM + Kotlin Coroutine + Flow + Room
  • 多 module 设计,公共模块与项目模块分离
  • 支持多引擎同步翻译,支持插件开发、下载、更新、导入、导出等,高拓展性
  • 充分利用 Kotlin 语言特性,如延迟懒加载、类代理、Coroutine、Flow、密闭类、拓展方法、拓展属性、反射等
  • 基于 Rhino JS 设计并实现完整的 JavaScript 插件加载、运行、调试环境,提供插件开发一条龙支持
  • 适配 Android11文件权限,适配双语、暗黑模式、横屏/平板页面,开发、使用并公开发布三个开源库

应用逐步适配 Android13,具体包括:

  • 支持单独设置应用语言

您可以在以下途径获取最新版本:

详细内容请查看 GitHub 仓库

挑点啥说一说

项目开始时觉得没什么,但是真的写起来却发现不少坑。下面简单挑一个例子。

导航栏内容与底部同步

软件的导航栏是基于Row自己定义的,摘录部分如下:

@ExperimentalAnimationApi
@Composable
fun CustomNavigation(
screens : Array<TranslateScreen>,
currentScreen: TranslateScreen = screens[0],
onItemClick: (TranslateScreen) -> Unit
) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.background)
.padding(start=8.dp,end=8.dp,bottom = 4.dp,top = 2.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
) {
screens.forEach{ screen->
CustomNavigationItem(item = screen, isSelected = currentScreen==screen) {
onItemClick(screen)
}
}
}
}

UI实现并不复杂,问题在于用它导航的时候

首先是点击Icon跳转到对应页面,这一部分理论上并不复杂,用navController.navigate(screen.route)就能达到最简单的效果

然后第一个问题出现了:每次进行navigate的时候,都会重新创建对应的Screen,导致之前页面输入的内容啥的都会被清空;而且每一次点开对应页面就会有一个新的,导致返回的时候需要疯狂点击回退,在不同页面间反复横跳……这肯定不行

怎么办呢?参考官方文档,说是加上launchSingleTop=true即可,也就是改成下面这样

navController.navigate(screen.route){
launchSingleTop = true
}

然后……然后居然还是不行!我不得其解,终于在过了几天后找到了有效的写法:

navController.navigate(screen.route){
//当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页
popUpTo(navController.graph.startDestinationId){
saveState = true
}
//从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例
launchSingleTop = true
//切换状态的时候保存页面状态
restoreState = true
}

这应该是从一篇博客上看到的,具体哪篇现在也找不到了。在此表示感谢!

上面的问题解决了,新的问题又来了:点击返回键时,导航栏也应该跟着变动,回到主页面。这一部分的需求又该怎么实现呢?

最终在开源项目里面一顿乱翻,终于找到了一个解决方案:在NavController.OnDestinationChangedListener回调中使用hierarchy进行匹配,让页面内容与底部导航栏始终保持同步。相关代码如下:

    val selectedItem = remember { mutableStateOf<TranslateScreen>(TranslateScreen.MainScreen) }

DisposableEffect(this) {
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
when {
destination.hierarchy.any { it.route == TranslateScreen.MainScreen.route } -> {
selectedItem.value = TranslateScreen.MainScreen
}
// ...省略类似的几个
destination.hierarchy.any { it.route == TranslateScreen.ThanksScreen.route } -> {
selectedItem.value = TranslateScreen.ThanksScreen
}
}
}
addOnDestinationChangedListener(listener)

onDispose {
removeOnDestinationChangedListener(listener)
}
}

顺便用BackHandler实现了退出时二次确认:

BackHandler(enabled = true) {
if (navController.previousBackStackEntry == null){
val curTime = System.currentTimeMillis()
if(curTime - activityVM.lastBackTime > 2000){
scope.launch { scaffoldState.snackbarHostState.showSnackbar(FunnyApplication.resources.getString(R.string.snack_quit)) }
activityVM.lastBackTime = curTime
}else{
exitAppAction()
}
}else{
Log.d(TAG, "AppNavigation: back")
//currentScreen = TranslateScreen.MainScreen
}
}

这部分完整代码在这:FunnyTranslation/TransActivity.kt

这样类似的地方在应用开发时还有很多,我相信这里的各位能有同感,就不在这搞吐槽大会了。不过既然尝试了一门新技术,就得做好万事碰壁自己探索的准备。这样也才有点 Coder 的精神嘛~

一些亮点

  • 持续更新:软件目前已持续维护两年多,各类库版本均在不断升级
  • Compose 与 View 的互操作:由于部分功能已有较好的 View 实现,故也有少量控件直接使用 View(例如软件的底部导航栏、插件代码编辑器),可供简略参考
  • Compose 实现的简易游戏:纯 Compose 实现了个简单的小游戏,内包括 自定义布局、手动倒计时 等操作,可以点击这里 查看
  • “ViewPager + TabLayout”:应用内常见的效果,用在了登录注册页面,可以点击 这里 查看
  • 自定义带圆形进度条的按钮:“开始翻译”的小按钮自带进度条,使用 drawWithContent 实现,可以点击 这里 查看

如果觉得项目有用的话,欢迎 star~