ARouter 入门指南:从基本跳转到对象传递

配置

在模块级的 build.gradle.kts 文件中进行配置:

kotlin 复制代码
plugins {
    id("kotlin-kapt")
}

android {
    buildFeatures {
        buildConfig = true
    }
}

dependencies {
    implementation("com.alibaba:arouter-api:1.5.2")
    kapt("com.alibaba:arouter-compiler:1.5.2")
}

kapt {
    arguments {
        // 每个模块必须配置模块名,用于编译期生成 Group 映射文件
        arg("AROUTER_MODULE_NAME", project.name)
    }
}

如果出现了依赖性冲突,可以在 gradle.properties 文件中添加这两行:

kotlin 复制代码
android.useAndroidX=true
android.enableJetifier=true // 让第三方库迁移到 AndroidX

初始化

配置完成后,在 Application 中初始化 ARouter

kotlin 复制代码
class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initARouter()
    }

    private fun initARouter() {
        // 必须在 init 之前配置
        if (BuildConfig.DEBUG) {
            ARouter.openLog()     // 打印日志
            ARouter.openDebug()   // 开启调试模式
        }
        ARouter.init(this)
    }
}

开启调试模式是为了让每次代码修改都能立即生效。因为 ARouter 在应用版本号不变时,会使用本地缓存,这就可能导致了路由路径修改后,运行时出现"找不到路径"的问题。

因此,我们强制它每次启动时都重新加载路由表,不使用缓存,这样每次拿到的都是最新构建的路由表。

路由与注入

页面定义与跳转

定义页面 (Destination) 的规范为:/组名/页面名

ARouter 会根据组名进行内存懒加载。

创建一个 UserProfileActivity,并定义它的路由。

kotlin 复制代码
@Route(path = "/user/profile") // 注意路径不能少于两级
class UserProfileActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)
    }
}

接着在 MainActivity 中实现最简单的跳转:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.go_btn).setOnClickListener {
            ARouter.getInstance().build("/user/profile").navigation(this)
        }
    }
}

运行结果:

参数传递与自动注入

需要传递参数的话,只需调用 withXxx 方法即可。

例如:

kotlin 复制代码
// MainActivity 的 onCreate 方法中
findViewById<Button>(R.id.go_btn).setOnClickListener {
    ARouter.getInstance().build("/user/profile")
        .withString("name", "Steven")
        .withLong("id", 10086L)
        .navigation(this)
}

接收方代码:

kotlin 复制代码
@Route(path = "/user/profile")
class UserProfileActivity : AppCompatActivity() {

    @JvmField
    @Autowired(name = "id")
    var userId: Long = 0L

    @JvmField
    @Autowired(name = "name")
    var userName: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)

        // 必须进行注入,否则所有字段都是默认值
        ARouter.getInstance().inject(this)
        
        Toast
            .makeText(
                this,
                "id: $userId, name: $userName",
                Toast.LENGTH_SHORT
            )
            .show()
    }
}

运行结果:

跳转并获取返回结果

如果要进行带结果的跳转,只需在跳转时,传入 requestCode 参数即可。

kotlin 复制代码
// MainActivity
companion object {
    private const val REQUEST_CODE_PROFILE = 100
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    findViewById<Button>(R.id.go_btn).setOnClickListener {
        ARouter.getInstance().build("/user/profile")
            .withString("name", "Steven")
            .withLong("id", 10086L)
            .navigation(this, REQUEST_CODE_PROFILE)
    }
}

在目标页面使用原生的 setResult 方法返回结果:

kotlin 复制代码
// UserProfileActivity
lifecycleScope.launch {
    delay(2000)
    val intent = Intent().apply {
        putExtra("updated_name", "Steven_666")
    }
    setResult(RESULT_OK, intent)
    finish()
}

在最初的页面重写 onActivityResult 方法来接收返回的数据:

kotlin 复制代码
override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    data: Intent?
) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == REQUEST_CODE_PROFILE && resultCode == RESULT_OK) {
        val updatedName = data?.getStringExtra("updated_name")
        Toast.makeText(this, "用户名已更新为:$updatedName", Toast.LENGTH_SHORT).show()
    }
}

拦截器

我们可以在跳转前进行拦截、修改和重定向。

比如在进到用户个人中心前进行拦截,我们要检查该用户的信息完整性,如果缺少,就拦截该跳转,引导到完善信息页面;如果完整,就放行。

kotlin 复制代码
@Interceptor(priority = 10, name = "用户资料完整性检查")
class UserProfileInterceptor : IInterceptor {

    private var mContext: Context? = null

    /**
     * 注意:该方法运行在子线程
     */
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.path == "/user/profile") {
            Log.i("ARouter", "正在检查进入个人中心的权限...")

            //检查本地是否有昵称
            val hasNickname = fakeCheckUserNicknameFromDb()

            if (hasNickname) {
                // 通过,继续跳转
                callback.onContinue(postcard)
            } else {
                // 失败,进行拦截
                callback.onInterrupt(RuntimeException("用户信息不完整"))

                //切换到主线程来显示 Toast 或跳转
                MainLooper.runOnUiThread {
                    Toast.makeText(mContext, "请先完善用户信息", Toast.LENGTH_SHORT).show()
                    // 重定向到完善信息页
                    ARouter.getInstance().build("/user/complete_info").navigation()
                }

                // 流程结束
                callback.onInterrupt(null)
            }
        } else {
            // 如果不是我们要拦截的路径,直接放行
            callback.onContinue(postcard)
        }
    }

    /**
     * 模拟从数据库查询用户昵称
     */
    private fun fakeCheckUserNicknameFromDb(): Boolean {
        val context = mContext ?: return false

        try {
            Log.d("ARouter", "正在查询数据库...")
            Thread.sleep(300)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }

        val sp = context.getSharedPreferences("user_data", Context.MODE_PRIVATE)
        val nickname = sp.getString("nickname", "")

        // 如果昵称不为空,则视为信息完整
        return !nickname.isNullOrEmpty()
    }

    /**
     * 初始化方法
     * 在 ARouter 初始化时被调用,运行在主线程
     */
    override fun init(context: Context) {
        // 这里可以做一些静态资源的加载,或者注册广播等(非耗时操作)
        this.mContext = context.applicationContext
        Log.i("ARouter", "UserProfileInterceptor 已加载")
    }
}

// 切换主线程的简单封装
object MainLooper {
    private val handler = Handler(Looper.getMainLooper())
    fun runOnUiThread(action: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            action()
        } else {
            handler.post(action)
        }
    }
}

实战结构参考

在大型项目中,我们不会硬编码路由字符串,也不允许让跳转逻辑分散。

我们会创建路由常量:

kotlin 复制代码
object RoutePath {
    private const val GROUP = "/user"
    const val USER_PROFILE = "$GROUP/profile"
    const val COMPLETE_USER_INFO = "$GROUP/complete_info"
}

然后封装一个导航中心 NavigationHelper

kotlin 复制代码
object NavigationHelper {

    /**
     * 通用跳转
     */
    fun startActivity(path: String, bundle: Bundle? = null) {
        ARouter.getInstance().build(path)
            .with(bundle)
            .navigation()
    }

    /**
     * 带结果的跳转
     */
    fun startForResult(activity: Activity, path: String, requestCode: Int) {
        ARouter.getInstance().build(path)
            .navigation(activity, requestCode)
    }
    
    /**
     * 导航用户详情页
     */
    fun toUserProfile(userId: Long, userName: String?) {
        ARouter.getInstance().build(RoutePath.USER_PROFILE)
            .withLong(UserProfileActivity.USER_ID, userId)
            .withString(UserProfileActivity.USER_NAME, userName ?: "")
            .navigation()
    }
}

现在我们无需知道参数 Key 的值,甚至无需知道用户详情页的路由路径。

现在的 MainActivity

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.go_btn).setOnClickListener {
            NavigationHelper.toUserProfile(10086L, "Steven")
        }
    }
}

现在的 UserProfileActivity

kotlin 复制代码
@Route(path = RoutePath.USER_PROFILE)
class UserProfileActivity : AppCompatActivity() {

    @JvmField
    @Autowired(name = USER_ID)
    var userId: Long = 0L

    @JvmField
    @Autowired(name = USER_NAME)
    var userName: String? = null

    companion object {
        const val USER_ID = "id"
        const val USER_NAME = "name"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_profile)

        ARouter.getInstance().inject(this)
        Toast
            .makeText(
                this,
                "id: $userId, name: $userName",
                Toast.LENGTH_SHORT
            )
            .show()
    }
}

Fragment 与对象传递

获取 Fragment 实例

kotlin 复制代码
@Route(path = "/home/fragment")
class HomeFragment : Fragment() {

}

加载 Fragment,也可以通过 ARouter 获取其实例。

kotlin 复制代码
// MainActivity
val fragment = ARouter.getInstance().build("/home/fragment").navigation() as? Fragment
if (fragment != null) {
    // 添加到 FragmentManager
    supportFragmentManager.beginTransaction()
        .add(R.id.xxx, fragment)
        .commit()
}

JSON 服务具体实现

现在,我们使用 .withObject 传递自定义对象时,会报错:java.lang.NullPointerException

因为没有进行序列化,接收端拿到的数据是 null。我们可以配置序列化服务来解决。

添加依赖:implementation("com.google.code.gson:gson:2.13.2") // Gson

kotlin 复制代码
@Route(path = "/service/json") // Path 可以自定义
class JsonServiceImpl : SerializationService {

    private var gson: Gson? = null

    override fun init(context: Context?) {
        gson = Gson()
    }

    @Deprecated("Deprecated in Java")
    override fun <T : Any?> json2Object(
        input: String?,
        clazz: Class<T?>?
    ): T? {
        return input?.let { gson?.fromJson(it, clazz) }
    }

    override fun object2Json(instance: Any?): String {
        return gson?.toJson(instance) ?: ""
    }

    override fun <T : Any?> parseObject(
        input: String?,
        clazz: Type?
    ): T? {
        return input?.let { gson?.fromJson(it, clazz) }
    }

}

之后,ARouter 会自动调用 JsonServiceImpl 来处理所有的 withObject 请求。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android