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 请求。

相关推荐
用户69371750013841 小时前
17.Kotlin 类:类的形态(四):枚举类 (Enum Class)
android·后端·kotlin
h***34631 小时前
MS SQL Server 实战 排查多列之间的值是否重复
android·前端·后端
用户69371750013842 小时前
16.Kotlin 类:类的形态(三):密封类 (Sealed Class)
android·后端·kotlin
摆烂积极分子4 小时前
安卓开发学习-安卓版本
android·学习
n***26565 小时前
MySQL JSON数据类型全解析(JSON datatype and functions)
android·mysql·json
t***82115 小时前
mysql的主从配置
android·mysql·adb
YF02117 小时前
Frida如何稳定连接PC端跟Android手机端
android·mac·xposed
O***P5718 小时前
【MySQL】MySQL内置函数--日期函数字符串函数数学函数其他相关函数
android·mysql·adb
z***43848 小时前
MySQL-mysql zip安装包配置教程
android·mysql·adb