Android架构设计(模块化+组件化+MVI)

Architecture-Android

功能介绍

  • 支持配置变更 后的还原
    • 屏幕旋转
    • 亮暗主题切换
    • 语言切换(国际化)
    • 字体大小更改
    • 分屏
    • ...
  • 支持进程杀死后的还原
  • 项目架构:模块化+组件化+MVI(UiState+ViewModel+Flow+Kotlin协程+Repository+DataSource+Retrofit)
  • 支持多App开发
  • 支持一键切换Feature模块单独运行
  • 支持一键去除可移除功能代码
  • 支持项目无反射实现(项目默认无反射实现,反射实现也提供,可供在两者选择)
  • 支持EdgeToEdgetargetSdk >= 35(Android15),强制开启了,所以为了开启时兼任低版本,需要全部支持)
  • 支持动态主题Android12+支持此功能)
  • 支持刷新、自动预加载(自动预加载,如果用户滑动慢并且获取数据快,用户是感觉不到加载的)

本项目为一个Android架构 ,它遵循 Android 设计开发最佳实践 ,旨在为开发者提供实用参考

本项目目标 是为了同时 支持ComposeView,以支持公司项目 目前已有的View代码和之后的Compose代码,目前本项目 支持View,后续项目关注度 (Star点赞数) 高后 会支持Compose

本项目是以字节跳动 公司的抖音 App为参照,模拟开发的抖音 App。由于本项目,无抖音真正的网络数据 ,所以本项目使用的数据,是通过某些开源API 网络接口,模拟 转的网络数据

本项目是在官方的架构nowinandroid(18.2k Star)architecture-samples(44.9k Star))上做的升级和修改 ,如果大家对此架构模块的划分 不理解,建议大家先了解官方的 nowinandroid,然后再来看本项目。

本项目文档分为快速介绍详细介绍模块间架构模块内架构 )、使用 ,建议大家按照顺序阅读文档。如果你想快速 的了解,可以只看快速介绍使用 ,我提供了demo工程 ,里面有最简单使用案例,可以先阅读此代码。

欢迎大家一起来维护 项目,使其功能更加的强大健硕 。有问题,有需求,请提issue,或者私信我。

项目链接: architecture-android欢迎大家点赞、收藏,以方便您后续查看。

下载

Apk下载

扫码下载

截图

App展示

主题展示

Themes Light Dark
抖音主题
动态主题1
动态主题2

快速介绍

下载

下载项目并运行

shell 复制代码
git clone [email protected]:zrq1060/architecture-android.git

功能演示

本项目任何页面,都支持如下:

  • 配置变更后的还原屏幕旋转亮暗模式切换语言切换字体大小更改分屏 等),配置变更会导致ActivityFragment重新创建新的
  • 进程被杀死后的还原(可打开,开发者选项-后台进程限制-不允许后台进程,以更好的测试进程被杀死。开启后,可在后台多打开一些无关的app,再切换打开此app即可演示此效果)。

本项目,目前仅支持如下功能:

  • 登录页 :登录账号(手机号、邮箱)为 【任意】 ,登录密码(验证码、密码)为 【123456】可断网 ,或输入错误密码,查看页面效果。

  • Home首页 :顶部栏目的排序(长按首页-顶部栏目

  • Main主页 :好友、商场栏目的切换(长按主页-底部栏目第2个

  • Shop商城页 :支持刷新自动加载点击商城条目 模拟的商城列表数据的增、删、改 操作。可断网,查看页面效果。

在此跟着上面,操作App支持的功能(记得开启屏幕自动旋转、切换亮暗模式、点击商城Item),以演示上面功能效果。

单独运行Feature模块演示

1、修改项目根目录gradle.propertiesisFeatureSingletrue,并Sync同步Gradle

groovy 复制代码
isFeatureSingle = true

2、执行安装全部命令

点击右侧Gradle-Tasks-install-installDebug执行如下命令:

groovy 复制代码
.\gradlew installDebug

执行完后,会在手机桌面 出现所有Feature模块App(如下图所示),点击 某个即可测试某单个Feature模块。

详细介绍

我们先讲模块间的架构 ,然后再讲模块内的架构 ,最后讲使用

模块间架构

在不断变大的代码库中,可扩缩性可读性整体代码质量 通常会随着时间的推移而降低。这是因为代码库在不断变大,而其维护者未采取积极措施来保持易于维护的结构。模块化是一种行之有效的代码库构建方法,可帮助改善可维护性并避免此类问题。

模块划分

标准版

本项目官方(标准版)模块化 完成后,模块图(部分模块) 如下:

模块说明:

  • app模块app模块依赖于所有feature模块和必需core模块。
  • feature:模块feature模块不应该依赖于其它的 feature模块,它们只依赖于所需core模块。
  • core:模块core模块可以依赖于其它的 core模块,但它们不应该依赖于 feature模块或app模块。

本项目官方模块化 完成后,项目目录图 如下(标准版):

多App版

一般一个公司并非一个App ,比如商城功能App (一个用户端、一个商家端)、外卖功能App (一个用户端、一个骑手端)。以字节跳动 公司为例子,其中公司开发的App有抖音西瓜视频今日头条飞书剪映等。

上面官方(标准版)模块 划分,导致内部的core模块含有本App特有 的、所有App通用 的代码及资源,仅适用于单App 架构。如果要适用多App 架构,就需要把core模块内所有App通用部分 提取出来,提取后的项目,项目目录图 如下(多App版):

变化说明:

  1. 标准版抖音 的模块结构,存到了最外层 douyin目录
  2. 把一些可所有App共用 的模块,存到了最外层 core目录
  3. 标准版西瓜视频 的模块结构,存到了最外层 xigua目录

目录说明:

  • 最外层 core目录 :为所有App 都可以使用的代码及资源,内部模块所有App 内的core模块依赖。
  • 最外层 douyin目录 :为 抖音App自己独有(特有) 的相关代码及资源。
    • core模块 :依赖最外层 core目录 内的模块,反之不行
    • app模块feature模块 :直接依赖抖音内部 core模块即可,此为最外层 core目录功能定制 ,如:直接依赖抖音App:douyin:core:architecture模块即可,此模块为抖音App:core:architecture模块(所有App通用的-架构模块 )的定制
  • 最外层 xigua目录 :为西瓜视频App的相关代码,规则同上(抖音)。

说明:还可以在最外层 继续开发其它App,如今日头条飞书剪映等,规则同上(抖音、西瓜视频)。

可移除版

在项目开发过程中,如果你不看好 要开发的功能,或者领导、产品告诉你,要开发的功能之后可能会移除,你可以使用此设计。

抖音App 为例,最早抖音 是没有商城 功能的,如果以商城 功能之后会移除来开发,你可以使用以下模块设计。

现在的项目,项目目录图 如下(可移除版):

变化说明:

  1. 可能要移除的shop功能 的模块结构,存到了最外层 douyin目录

新增的shop目录说明:

  • core模块可以依赖抖音、商城core模块,但是抖音core模块不能依赖商城core模块(以便好在抖音内移除)
  • feature模块同级feature间不能相互依赖,只能依赖抖音、商城core模块

可以修改项目根目录gradle.propertiesisShopIncludefalseisRouterReflecttrue,来演示此移除功能

groovy 复制代码
isShopInclude = false
isRouterReflect = true

说明:

isShopInclude:为是否包括商城

isRouterReflect:为是否Router反射实现

  • 本项目Router的实现分为了两种,Dagger实现正式 用)、反射实现测试 用),详细看router模块。

模块包名

包名格式一般为:域名反转+项目名+功能名 ,以此字节跳动 (域名:www.bytedance.com )公司抖音项目为例,规则如下:

  • corecom.bytedance.core.xxx和特定App无关),如:com.bytedance.core.architecture
  • douyin:com.bytedance.douyin.xxx和特定App有关
    • app:com.bytedance.douyin
    • core:com.bytedance.douyin.core.xxx,如:com.bytedance.douyin.core.architecture
    • feature:com.bytedance.douyin.feature.xxx,如:com.bytedance.douyin.feature.home
    • shop:com.bytedance.douyin.shop .xxx
  • xigua:规则同上(抖音)

模块功能

  • app:项目的入口,含有MainActivityApplication等。
  • core
    • architecture:架构相关,包含一些基础类,如:最外层:core:architecture模块包含通用的BaseViewsActivityBaseViewsFragment等,抖音层:douyin:core:architecture模块包含抖音 定制的AppViewsActivityAppViewsFragment等。
    • architecture-reflect:架构反射实现相关,包含一些架构内的反射实现,如:reflectInflateViewBinding(反射实现ViewBinding)、reflectViewModels(反射实现ViewModel)。
    • common:通用相关,包含一些通用类、工具类等。
    • designsystem:设计系统相关,包含控件、主题等。
    • modelModel类相关,包含Model类等。
    • network:网络相关,包含NetworkDataSource、网络工具类、图片加载等。
    • test:测试页面相关(为了给未实现的功能,占位用),包含TestActivityTestFragment等。
    • webview:网页相关,包含网页的跳转、配置等。
    • data:数据相关,包含Repository类等。
    • datastoreDataStore存储相关,包含PreferencesDataSource等。
    • datastore-protoDataStoreproto配置相关,包含user.proto配置等。
    • feature-single:单独模块运行通用配置相关,包含TestFragmentDetailsAndroidEntryPointActivity等。
    • login:登录相关,包含登录检测、当前登录状态、退出登录等。
    • router:路由系统相关,包含RouterDagger实现反射实现等。
  • feature:功能业务,包含UI、ViewModel等。

Feature模块间通信介绍

Feature模块间通信,使用router模块的Router类进行通信,以home模块为例规则如下:

定义
kotlin 复制代码
interface HomeRouter {
    fun createHomeFragment(): Fragment
}

HomeRouter接口为home模块对外暴露可供其它模块调用 部分,在router模块内定义,如果还有其它的,可继续在此接口内添加,如:createXXXFragmentstartXXXActivity方法等。

真的实现
kotlin 复制代码
class DefaultHomeRouter : HomeRouter {
    override fun createHomeFragment(): Fragment = HomeFragment.newInstance()
}

DefaultHomeRouter类为HomeRouter接口真的实现 ,在home模块内实现。

假的实现
kotlin 复制代码
class FakeHomeRouter : HomeRouter {
    override fun createHomeFragment(): Fragment = AppTestFragment.newInstance("Home")
}

FakeHomeRouter类为HomeRouter接口假的实现 ,在router模块的router-reflect内实现,内部使用的AppTestFragment仅是为了显示时占位用

说明:

Router-Dagger实现 :使用DaggerHomeRouter的实现(目前提供的是DefaultHomeRouter),如果找不到会报错

Router-反射实现 :使用反射 直接找DefaultHomeRouter,如果找不到会直接使用FakeHomeRouter不会报错

调用
kotlin 复制代码
val homeFragment = Router.Home.createHomeFragment()

单独运行Feature模块介绍

如果你只负责某个Feature模块,或者想更解耦更快 的测试你的功能,你可以使用此单独运行Feature模块,步骤如下:

修改配置

修改项目根目录gradle.propertiesisFeatureSingletrue,并Sync同步Gradle

groovy 复制代码
isFeatureSingle = true

说明:

isFeatureSingle:为是否单独运行Feature模块 。如果开启 ,则Router使用反射实现 ,以使其调用其它模块 没有时不会报错 ,而是使用Fake的实现(如:占位显示)。

添加测试入口点

此功能需要配合使用我的TestPoint库来实现,添加测试入口点 ,即会在测试列表页 增加一个按钮,点击按钮跳转到此ActivityFragment定制按钮点击 等详细使用请看TestPoint

在目标类上添加TestEntryPoint注解,如ShopFragment

kotlin 复制代码
@TestEntryPoint("商城")
class ShopFragment{

}
运行

单个运行:

选择上面的一个,并运行,如:选择douyin.shop.feature.shop,则运行抖音的商城功能 (可以测试商城的点击Item功能等)。

多个运行:

点击右侧Gradle-Tasks-install-installDebug执行如下命令:

groovy 复制代码
.\gradlew installDebug

执行完后,会在手机桌面 出现所有Feature模块App(如快速介绍-单独运行Feature模块演示图所示),点击 某个即可测试某单个Feature模块。

模块内架构

官方架构

模块内架构,使用官方推荐架构,有助于构建强大而优质的应用。

应用架构 相关,请看官方的 应用架构指南

官方的架构概述图 如下:

官方架构分为了:UI层Domain层 (可选)、Data数据层

项目架构

本项目,目前没有使用 Domain层,也没有使用 Room库,目前的项目架构图 如下:

使用

Activity、Fragment

demo模块的MainActivity为例:

kotlin 复制代码
package com.bytedance.demo.app.main

import android.view.LayoutInflater
import androidx.activity.viewModels
import com.bytedance.douyin.core.architecture.app.views.AppViewsActivity
import dagger.hilt.android.AndroidEntryPoint
// 设置as别名,一般都是设置这几个。
// 使用别名后,此类的模板,下面的不需要改了,只需要改上面as这里即可。
import com.bytedance.demo.app.main.MainUiState as UiState
import com.bytedance.demo.app.main.MainViewModel as ViewModel
import com.bytedance.demo.databinding.ActivityMainBinding as ViewBinding

/**
 * 描述:
 *
 * @author zhangrq
 * createTime 2025/3/24 11:14
 */
@AndroidEntryPoint
class MainActivity : AppViewsActivity<ViewBinding, UiState, ViewModel>() {
    // 在父类AppViewsActivity中,可用反射实现(reflectViewModels()),省略此实现。
    override val viewModel: ViewModel by viewModels()

    // 在父类AppViewsActivity中,可用反射实现(reflectInflateViewBinding()),省略此实现。
    override fun inflateViewBinding(inflater: LayoutInflater) = ViewBinding.inflate(inflater)

    // 初始化View(可以在里面直接拿到当前页面布局控件)
    override fun ViewBinding.initViews() {
        // 设置TextView控件
        content.textSize = 50f
//        content.setTextColor(Color.BLACK)
    }

    // 初始化Listener(可以在里面直接拿到当前页面布局控件)
    override fun ViewBinding.initListeners() {
        // 设置TextView点击
        content.setOnClickListener {
            // 显示Toast,此Toast和当前页面的生命周期绑定,当前页面不可见,Toast关闭。
            viewModel.showMessage("Long Toast", isShort = false)
        }
    }

    // 初始化Observer(可以在里面直接拿到当前页面布局控件),用于观察(收集)ViewModel内的暴露的属性值(Flow值)。
    override fun ViewBinding.initObservers() {
    }

    // 收集UiState的值(可以在里面直接拿到当前页面布局控件),用于设置当前页面的数据。
    override fun ViewBinding.onUiStateCollect(uiState: UiState) {
        // 设置TextView的值
        content.text = uiState.tabs?.joinToString()
    }
}

ActivityFragmentDialogFragment使用规则相同 ,以Activity为例,说明如下:

  1. MainActivity直接继承App级AppViewsActivity,此类为抖音 项目对通用级BaseViewsActivity定制
  2. ViewModelViewBinding创建 ,由于本项目为了性能没有使用反射 ,所以需要在每个子类中自己实现 ,可以在App级AppViewsActivity内使用reflectInflateViewBindingreflectViewModels反射实现 ,这样就可以在每个子类 中省略ViewModelViewBinding创建代码
  3. 初始化系列方法 ,使用ViewBinding扩展方法,是为了能让其在方法内直接获取到 xxx控件 ,而不用通过binding.xxx获取,以更方便操作控件
  4. XXXBindingXXXUiStateXXXViewModel,全部通过as别名 来命名,简化了名字长度统一了代码样式一致性 ,这样新类 只需要修改模板类上面as别名即可。

ViewModel

demo模块的MainViewModel为例:

kotlin 复制代码
package com.bytedance.demo.app.main

import com.bytedance.douyin.core.architecture.app.AppViewModel
import com.bytedance.douyin.core.data.repository.interfaces.MainRepository
import com.bytedance.douyin.core.model.MainTabType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
// 设置as别名
import com.bytedance.demo.app.main.MainUiState as UiState

/**
 * 描述:
 *
 * @author zhangrq
 * createTime 2025/3/24 11:14
 */
@HiltViewModel
class MainViewModel @Inject constructor(mainRepository: MainRepository) : AppViewModel<UiState>() {

    override val uiStateInitialValue: UiState = UiState() // UiState-初始化值

    // 从MainRepository获取的本地流,本地数据改,UI改。
    override val uiStateFlow: Flow<UiState> = mainRepository.getMainTabsStream().map {
        // UiState-页面值
        UiState(tabs = it)
    }
}

// Main-UiState
data class MainUiState(
    val tabs: List<MainTabType>? = null,
)

说明:

  1. ViewModel直接继承App级AppViewModel,此类为抖音 项目对通用级BaseViewModel定制
  2. uiStateInitialValueUiState的初始化值,一般为默认的 UiState对象
  3. uiStateFlowUiStateFlow流,它变化会影响到ActivityFragmentonUiStateCollect(),一般为Flow单个 Flow、使用combine()观察多个 Flow)的map()转为UiStateFlow
  4. XXXUiState,通过as别名 来命名,简化了名字长度统一了代码样式一致性 ,这样新类 只需要修改模板类上面as别名即可。

生命周期Toast、Snackbar

直接显示 ToastSnackbar,是没有生命周期控制 的(只负责显示 ),即使ActiviyFragment不可见 (被销毁、回到后台),也还在显示 。我增加了生命周期消息显示 ,仅在ActiviyFragment可见时显示

指定消息的显示

指定消息的显示 ,是使用Toast,还是Snackbar,目前默认Toast

  • 全局消息指定 ,在BaseGlobalMessageInitializer类设置。
  • 生命周期消息指定 ,在App级的AppViewsActivityAppViewsFragmentAppViewsDialogFragment重写 messageCollector的实现。

使用消息

kotlin 复制代码
// 全局消息,不受Activiy、Fragment的生命周期影响。
MessageManager.showGlobalMessage("Global Message")
// 生命周期消息,受viewModel的Activiy、Fragment的生命周期影响。
viewModel.showMessage("Short Message")
viewModel.showMessage("Long Message", isShort = false)

StateView

StateView包含多个状态形式View 的接口,状态包括:LoadingErrorEmptySuccess

定制UI

定制UI :目前实现StateView接口的类是DefaultStateView

  • 小改DefaultStateView默认 实现了LoadingErrorEmpty状态View,可修改指定某个定制UI
  • 大改 :可通过修改createAppStateView()createAppListStateView()方法,返回StateView接口的其它实现类

原理

  • 列表使用 :是使用BaseRecyclerViewAdapterHelperstateView实现,底层原理是给RecyclerView的Adapter添加了一条Item布局Empty状态 ,是通过返回的列表数据是否为空 来判断的,详细使用看BaseRefreshLoadMoreHelper
  • 普通使用 :是使用Base类 ActivityFragmentgetStateViewReplaceView()方法实现,底层原理是给此方法返回的View替换显示为StateViewEmpty状态目前未判断 ,如需修改请看BaseViewModel.requestAsyncBase()扩展方法。

使用

  • 列表使用已封装好 ,目前已支持SmartRefreshLayoutSwipeRefreshLayout两个控件,详细使用请看SmartRefreshLoadMoreHelperSwipeRefreshLoadMoreHelper
  • 普通使用 :需要使用BaseViewModel.requestAsyncBase()扩展方法定制
    • 配置ActivityFragment需要实现getStateViewReplaceView(),此为StateView替换的View (用于实现替换显示StateView 时,隐藏此View ),可通过覆写 此方法来修改StateView的显示范围 ,如果不覆写默认 为此ActivityFragmentroot根布局 。详细使用,请看通用级BaseViewsActivityBaseViewsFragment等。
    • 使用请求异步UI 每人的需求不同(如:Error状态,有人想要显示Error重试布局 ,有人想要只需要消息提示 ),定制 详细使用,请看BaseViewModel.requestAsyncBase()扩展方法。

刷新、自动加载

原理

使用

  1. UI层ActivityFragment实现类,需要使用SmartRefreshLoadMoreHelperSwipeRefreshLoadMoreHelper初始化,详细看ShopFragment
  2. ViewModel层ViewModel实现类,需要实现RefreshRepositoryOwner接口,其onRefreshRepository()方法需要返回刷新/刷新加载仓库。
  3. Repository层
    1. Repository实现类,需要实现 RefreshRepository仅刷新 )或RefreshLoadMoreRepository刷新加载)接口。
    2. Repository实现类,需要继承 PageKeyedMemoryRefreshLoadMoreRepository通过page加载 )或ItemKeyedMemoryRefreshLoadMoreRepository通过Item加载)类。

网络

一个公司,可能有多个网络规则 ,可创建实现BaseNetworkModel接口的XXXBaseNetworkModel 类,来实现此规则定制 功能,后续只需使用此类即可。目前项目内有2个规则 案例,请看ApiOpenBaseNetworkModelAppBaseNetworkModel类。

定义XXXBaseNetworkModel

以开源接口ApiOpen为例,其返回格式模板为:

json 复制代码
{"code": 200,  "message": "成功!",  "result": "string"}

code200代表公司的规则成功message提示的消息result结果(类型任意),以此创建类如下:

kotlin 复制代码
@Serializable
data class ApiOpenBaseNetworkModel<T>(val code: Int, val message: String, val result: T? = null) :
    BaseNetworkModel<T> {
    override fun isRuleSuccess() = code == 200

    override fun code() = code
    override fun message() = message
    override fun data() = result
}

使用XXXBaseNetworkModel

kotlin 复制代码
interface FakeNetworkLoginApi {
    /**
     * 登录
     */
    @POST("api/login")
    @FormUrlEncoded
    suspend fun login(
        @Field("account") account: String,
        @Field("password") password: String,
    ): ApiOpenBaseNetworkModel<FakeNetworkUser>
}

login()方法,其返回值为ApiOpenBaseNetworkModel,其泛型为json模板的result值。调用如下:

kotlin 复制代码
loginApi.login(account, password)

loginApi.login()方法,其返回值为ApiOpenBaseNetworkModel,这个数据不仅包含了json模块的全部信息 ,而且我们还得需要判断其是否公司规则成功

可以使用以下转换方法,转为自己想要的结果。

转换XXXBaseNetworkModel

kotlin 复制代码
loginApi.login(account, password).toRuleSuccessData()

toRuleSuccessData()方法,将ApiOpenBaseNetworkModel,转换为公司规则成功 ,并且返回其内部的result ,并且此返回值不为空

目前支持的,所有转换方法,如下:

kotlin 复制代码
/**
 * 网络成功-规则成功-内部数据-不可空
 */
fun <T> BaseNetworkModel<T>.toRuleSuccessData(): T {
    if (!isRuleSuccess()) {
        throw RuleException(code(), message())
    }
    return data()!!
}

/**
 * 网络成功-规则成功-内部数据-可空
 */
fun <T> BaseNetworkModel<T>.toRuleSuccessDataNullable(): T? {
    if (!isRuleSuccess()) {
        throw RuleException(code(), message())
    }
    return data()
}

/**
 * 网络成功-规则成功-全部数据
 */
fun <T> BaseNetworkModel<T>.toRuleSuccess(): BaseNetworkModel<T> {
    if (!isRuleSuccess()) {
        throw RuleException(code(), message())
    }
    return this
}
// 网络成功-全部数据。则不需要调用此转换方法,直接返回即可。

可根据自己的需求,使用自己想要的转换方法,一般为toRuleSuccessData()网络成功-规则成功-内部数据-不可空)。

未来支持

  • 支持Compose
  • 优化是否Login相关逻辑
  • 优化WebView相关逻辑

其它

三方库

自己

三方

参考

项目链接: architecture-android欢迎大家点赞、收藏,以方便您后续查看。

相关推荐
uhakadotcom9 分钟前
nginx的JavaScript魔力:njs简介与实践
javascript·后端·面试
FIT2CLOUD飞致云1 小时前
1Panel MCP Server发布,开启AI对话式运维新时代!
运维·开源
suke1 小时前
Qwen2.5-Omni 全能旗舰 VS 国产小钢炮 MiniCPM-V:参数、硬件、资源、优势全解析
人工智能·程序员·开源
用户3157476081351 小时前
MCP的出现,是对Function Calling的“书同文、车同轨”吗?
人工智能·面试·mcp
martian6653 小时前
分布式并发控制实战手册:从Redis锁到ZK选主的架构之道
java·开发语言·redis·分布式·架构
SimonKing4 小时前
JDK 24 新特性解析:更安全、更高效、更易用
java·后端·架构
Java技术小馆4 小时前
如何排查Linux系统中的CPU使用率过高问题
java·后端·面试
六月的可乐4 小时前
【干货】前端实现文件保存总结
前端·javascript·面试
蚝油菜花4 小时前
Math24o:SuperCLUE开源的高中奥数推理测评基准,85.71分屠榜
人工智能·开源