午后一小憩

午后一小憩 查看完整档案

上海编辑剑桥大学  |  未知 编辑未知  |  未知 编辑 www.rousetime.com 编辑
编辑

微信公众号:Android补给站

个人动态

午后一小憩 发布了文章 · 9月22日

重温Retrofit源码,笑看协程实现

cover.png

最近回归看了一下Retrofit的源码,主要是因为项目接入了协程,所以想研究一下Retorift是如何支持协程的。Retrofit是在Version 2.6.0开始支持协程的,所以本篇文章有关Retrofit的源码都是基于2.6.0的。

温馨提示,如果有Retrofit的源码阅读经验,阅读这篇文章将会轻松很多。

<!--放心你没有进错房间,这不是分析协程的文章,只是刚好谈到协程,所以还是简单说下Retrofit的实现。
不感兴趣的可以直接跳过下面的小插曲,放心不影响后续的阅读。-->

Retrofit

相信老鸟都应该很清楚,Retrofit核心部分是create()方法返回的动态代理(这里就不详细说明了,之后会有专门的文章分析动态代理)。就是下面这段代码:

  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();
          private final Object[] emptyArgs = new Object[0];

          @Override public @Nullable Object invoke(Object proxy, Method method,
              @Nullable Object[] args) throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
          }
        });
  }

第一眼看,跟我之前印象中的有点区别(也不知道是什么版本),return的时候居然没有adapt方法了。开始还以为有什么重大的改变,其实也没什么,只是将之前的adapt方法封装到invoke方法中。

相关的method注解解析都放到ServiceMethod中,有两个关键函数调用,分别是RequestFactoryHttpServiceMethodparseAnnotations()方法。

  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
 
    Type returnType = method.getGenericReturnType();
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(method,
          "Method return type must not include a type variable or wildcard: %s", returnType);
    }
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }
 
    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
  }

RequestFactory

首先RequestFactory中的parseAnnotations()最终通过build()方法来构建一个RequestFactory,用来保存解析出来的方法信息。

    RequestFactory build() {
      //1.解析方法上的注解
      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }
      ...
      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      //2.循环遍历方法中的各个参数,解析参数的注解
      for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
        parameterHandlers[p] =
            parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
      }
      ...
      return new RequestFactory(this);
    }

可以看到主要分为两步:

  1. 通过parseMethodAnnotation来解析出请求的方式,例如GETPOSTPUT等等;同时也会验证一些注解的合规使用,例如MultipartFormUrlEncoded只能使用一个。
  2. 通过parseParameter来解析出请求的参数信息,例如PathUrlQuery等等;同时也对它们的合规使用做了验证,例如QueryMapFieldMap等注解它们的key都必须为String类型。这些注解的解析都是在parseParameterAnnotation()方法中进行的。

上面的p == lastParameter需要特别注意下,为何要专门判断该参数是否为最后一个呢?请继续向下看。

协程的判断条件

下面我们来着重看下parseParameter的源码,因为从这里开始就涉及到协程的判断。

    private @Nullable ParameterHandler<?> parseParameter(
        int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
      ParameterHandler<?> result = null;
      if (annotations != null) {
        for (Annotation annotation : annotations) {
          //1.解析方法参数的注解,并验证它们的合法性
          ParameterHandler<?> annotationAction =
              parseParameterAnnotation(p, parameterType, annotations, annotation);

          if (annotationAction == null) {
            continue;
          }

          //每个参数都只能有一个注解
          if (result != null) {
            throw parameterError(method, p,
                "Multiple Retrofit annotations found, only one allowed.");
          }

          result = annotationAction;
        }
      }

      //2.判断是否是协程 
      if (result == null) {
        if (allowContinuation) {
          try {
            if (Utils.getRawType(parameterType) == Continuation.class) {
              isKotlinSuspendFunction = true;
              return null;
            }
          } catch (NoClassDefFoundError ignored) {
          }
        }
        throw parameterError(method, p, "No Retrofit annotation found.");
      }

      return result;
    }

第一点没什么好说的,里面没什么逻辑,就是一个纯注解解析与Converter的选取。

第二点是关键点,用来判断该方法的调用是否使用到了协程。同时有个allowContinuation参数,这个是什么呢?我们向上看,发现它是方法中的一个参数,如果我们继续追溯就会发现它就是我们之前特意需要注意的p == lastParameter

所以判断是否是使用了协程有三步:

  1. result为空,即该参数没有注解
  2. allowContinuationtrue,即是最后一个参数
  3. Continuation.class,说明该参数的类型为Continuation

只有符合上述三点才能证明使用了协程,但脑海里回想一下协程的写法,发现完全对不到这三点...

到这里可能有的读者已经开始蒙圈了,如果你没有深入了解协程的话,这个是正常的状态。

别急,要理解这块,还需要一点协程的原理知识,下面我来简单说一下协程的部分实现原理。

suspend原理

我们先来看下使用协程是怎么写的:

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

这是一个标准的协程写法,然后我们再套用上面的条件,发现完全匹配不到。

因为,这是不协程的本来面目。我们思考一个问题,为什么使用协程要添加suspend关键字呢?这是重点。你可以多想几分钟。

(几分钟之后...)

不吊大家胃口了,我这里就直接说结论。

因为在代码编译的过程中会自动为带有suspend的函数添加一个Continuation类型的参数,并将其添加到最后面。所以上面的协程真正的面目是这样的:

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): NewsResponse

现在我们再来看上面的条件,发现能够全部符合了。

由于篇幅有限,有关协程的原理实现就点到为止,后续我会专门写一个协程系列,希望到时能够让读者们认识到协程的真面目,大家可以期待一下。

现在我们已经知道了Retrofit如何判断一个方法是否使用了协程。那么我们再进入另一个点:

Retrofit如何将Call直接转化为NewResonse,简单的说就是支持使newsGet方法返回NewsResponse。而这一步的转化在HttpServiceMethod中。

HttpServiceMethod

上面已经分析完RequestFactoryparseAnnotations(),现在再来看下HttpServiceMethod中的parseAnnotations()

  static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
      Retrofit retrofit, Method method, RequestFactory requestFactory) {
    boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
    boolean continuationWantsResponse = false;
    boolean continuationBodyNullable = false;

    Annotation[] annotations = method.getAnnotations();
    Type adapterType;
    // 1. 是协程
    if (isKotlinSuspendFunction) {
      Type[] parameterTypes = method.getGenericParameterTypes();
      Type responseType = Utils.getParameterLowerBound(0,
          (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
      // 2. 判断接口方法返回的类型是否是Response
      if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
        // Unwrap the actual body type from Response<T>.
        responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
        continuationWantsResponse = true;
      } else {
        // TODO figure out if type is nullable or not
        // Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
        // Find the entry for method
        // Determine if return type is nullable or not
      }

      // 3. 注意:将方法返回类型伪装成Call类型,并将SkipCallbackExecutor注解添加到annotations中
      adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
      annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
    } else {
      adapterType = method.getGenericReturnType();
    }

    // 4. 创建CallAdapter,适配call,将其转化成需要的类型
    CallAdapter<ResponseT, ReturnT> callAdapter =
        createCallAdapter(retrofit, method, adapterType, annotations);
    Type responseType = callAdapter.responseType();
    // 5. 创建Converter,将响应的数据转化成对应的model类型
    Converter<ResponseBody, ResponseT> responseConverter =
        createResponseConverter(retrofit, method, responseType);

    okhttp3.Call.Factory callFactory = retrofit.callFactory;
    // 6. 接口方法不是协程
    if (!isKotlinSuspendFunction) {
      return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
    } else if (continuationWantsResponse) {
      // 7. 接口方法是协程,同时返回类型是Response类型
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
          callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
    } else {
      // 8. 接口方法是协程,同时返回类型是body,即自定义的model类型
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>(requestFactory,
          callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
          continuationBodyNullable);
    }
  }

代码中已经解析的很清楚了,需要注意3,如果是协程会做两步操作,首先将接口方法的返回类型伪装成Call类型,然后再将SkipCallbackExecutor手动添加到annotations中。字面意思就在后续调用callAdapter.adapt(call)时,跳过创建Executor,简单理解就是协程不需要Executor来切换线程的。为什么这样?这一点先放这里,后续创建Call的时候再说。

我们直接看协程的7,8部分。7也不详细分析,简单提一下,它就是返回一个Response<T>的类型,这个Retrofit最基本的支持了。至于如何在使用协程时将Call<T>转化成Response<T>原理与8基本相同,只是比8少一步,将它的body转化成对应的返回类型model。所以下面我们直接看8。

将Call转化成对应的Model

  static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
    private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
    private final boolean isNullable;

    SuspendForBody(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
        Converter<ResponseBody, ResponseT> responseConverter,
        CallAdapter<ResponseT, Call<ResponseT>> callAdapter, boolean isNullable) {
      super(requestFactory, callFactory, responseConverter);
      this.callAdapter = callAdapter;
      this.isNullable = isNullable;
    }

    @Override protected Object adapt(Call<ResponseT> call, Object[] args) {
      // 1. 获取适配的Call
      call = callAdapter.adapt(call);

      //noinspection unchecked Checked by reflection inside RequestFactory.
      // 2. 获取协程的Continuation
      Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
      return isNullable
          ? KotlinExtensions.awaitNullable(call, continuation)
          : KotlinExtensions.await(call, continuation);
    }
  }

我们的关注点在adapt,文章开头已经说了,新版的Retrofitadapt隐藏到invoke中。而invoke中调用的就是这个adapt

首先第一步,适配Call,如果是RxJava,这里的callAdapter就是RxJava2CallAdapter,同时返回的就是Observable,这个之前看过源码的都知道。

但现在是协程,那么这个时候的callAdapter就是Retrofit默认的DefaultCallAdapterFactory

  @Override public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    // 1. 注意: 如果是协程,因为接口方法返回没有使用Call,之前3的第一步伪装成Call的处理就在这里体现了作用
    if (getRawType(returnType) != Call.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalArgumentException(
          "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
    }
    final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);

    // 2. 之前3的第二部就在这里体现,由于之前已经将SkipCallbackExecutor注解添加到annotations中,所以Executor直接为null
    final Executor executor = Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
        ? null
        : callbackExecutor;

    return new CallAdapter<Object, Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public Call<Object> adapt(Call<Object> call) {
        // 3. 最终调用adapt时候返回的就是它本身的Call,即不需要进行适配。
        return executor == null
            ? call
            : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }

代码注释已经将之前3的作用解释完了,我们回到SuspendForBodyadpt,再看第二步。熟悉的一幕,又用到了最后的一个参数。这里的isNullable目前Retrofit的版本都是false,可能后续会支持空类型。但现在肯定是不支持的,所以直接进入KotlinExtensions.await()

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          // 1. 拿到body
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +
                method.declaringClass.name +
                '.' +
                method.name +
                " was null but response body type was declared as non-null")
            // 2. body为空,唤起协程,抛出异常
            continuation.resumeWithException(e)
          } else {
            // 3. 唤起协程,返回body
            continuation.resume(body)
          }
        } else {
          // 4. 唤起协程,抛出异常
          continuation.resumeWithException(HttpException(response))
        }
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        // 5. 唤起协程,抛出异常
        continuation.resumeWithException(t)
      }
    })
  }
}

看到这段代码,可能有的读者很熟悉,已经明白了它的转化。因为在Retrofit之前的几个版本,如果使用协程是不支持接口方法直接返回model的,需要返回Call<T>类型的数据。所以当时有的开源项目就是通过这个类似的extensions方法来转化成对应的model

遗憾的是,就是使用了RetrofitVersion 2.6.0之后的版本,我还是看到有的人使用这一套来自己转化,希望看到这篇文章的读者不要再做重复的事情,将其交给Retrofit自身来做就可以了。

上面的extensions作用就一个,通过suspendCancellableCoroutine来创建一个协程,它是协程中几个重要的创建协程的方法之一,这里就不细说,后续开协程系列在详细说明。

主要是onResponse回调,协程通过挂起来执行耗时任务,而成功与失败会分别通过resume()resumeWithExecption()来唤起挂起的协程,让它返回之前的挂起点,进行执行。而resumeWithExecption()内部也是调用了resume(),所以协程的唤起都是通过resume()来操作的。调用resume()之后,我们可以在调用协程的地方返回请求的结果。那么一个完美的协程接口调用就是这样实现的。

嗯,结束了,整理一下也不是很复杂吧。之后使用Retroift写协程时将通畅多了。

今天就这样吧,协程部分就分析到这里,Retrofit的整个协程实现部分就分析结束了,我将关键点都特别进行了标注与说明,希望对分析Retrofit的协程实现有所帮助。

最后,感谢你的阅读,如果你有时间的话,推荐带着这篇文章再去阅读一下源码,你将会有更深刻的印象。

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于Jetpack&DataBindingMVVM;项目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

为自己代言

微信搜索公众号:【Android补给站】或者扫描下方二维码进行关注

查看原文

赞 1 收藏 1 评论 2

午后一小憩 发布了文章 · 9月11日

Android Startup实现分析

1_lDGK7qz9h3xQP_IUoZxGaQ.png

前言

Android Startup提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用Android Startup来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。 与此同时,Android Startup支持同步与异步等待、手动控制依赖执行时机,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。

Android Startup经过几轮的迭代已经更加完善了,支持的功能场景也更加多样,如果你要使用Android Startup的新特性,请将依赖升级到最新版本latest release

dependencies {
    implementation 'com.rousetime.android:android-startup:latest release'
}

在之前的我为何弃用Jetpack的App Startup?文章中有提供一张与App Startup的对比图,现在也有了一点变化

指标App StartupAndroid Startup
手动配置
自动配置
依赖支持
闭环处理
线程控制
异步等待
依赖回调
手动通知
拓扑优化

核心内容都在这种对比图中,下面根据这种对比图来详细分析一下Android Startup的实现原理。

配置

手动

手动配置是通过StartupManager.Builder()来实现的,本质很简单,使用builder模式来初始化一些必要的参数,进而来获取StartupManager实例,最后再启动Android Startup

val config = StartupConfig.Builder()
    .setLoggerLevel(LoggerLevel.DEBUG)
    .setAwaitTimeout(12000L)
    .setListener(object : StartupListener {
        override fun onCompleted(totalMainThreadCostTime: Long, costTimesModels: List<CostTimesModel>) {
            // can to do cost time statistics.
            costTimesLiveData.value = costTimesModels
            Log.d("StartupTrack", "onCompleted: ${costTimesModels.size}")
        }
    })
    .build()
 
StartupManager.Builder()
    .setConfig(config)
    .addStartup(SampleFirstStartup())
    .addStartup(SampleSecondStartup())
    .addStartup(SampleThirdStartup())
    .addStartup(SampleFourthStartup())
    .build(this)
    .start()
    .await()

自动

另一种方式是自动配置,开发者不需要手动调用StartupManager.Builder(),只需在AndroidManifest.xml文件中进行配置。

<provider
    android:name="com.rousetime.android_startup.provider.StartupProvider"
    android:authorities="${applicationId}.android_startup"
    android:exported="false">

    <meta-data
        android:name="com.rousetime.sample.startup.SampleStartupProviderConfig"
        android:value="android.startup.provider.config" />

    <meta-data
        android:name="com.rousetime.sample.startup.SampleFourthStartup"
        android:value="android.startup" />

</provider>

而实现这种配置的原理是:Android Startup内部是通过一个ContentProvider来实现自动配置的,在AndroidContentProvider的初始化时机介于ApplicationattachBaseContextonCreate之间。所以Android Startup借助这一特性将初始化的逻辑都封装到自定义的StartupProvider

class StartupProvider : ContentProvider() {

    override fun onCreate(): Boolean {
        context.takeIf { context -> context != null }?.let {
            val store = StartupInitializer.instance.discoverAndInitialize(it)
            StartupManager.Builder()
                .setConfig(store.config?.getConfig())
                .addAllStartup(store.result)
                .build(it)
                .start()
                .await()
        } ?: throw StartupException("Context cannot be null.")

        return true
    }
    ...
    ...
}

有了StartupProvider之后,下一步需要做的就是解析在AndroidManife.xmlprovider标签下所配置的StartupConfig

有关解析的部分都在StartupInitializer类中,通过它的discoverAndInitialize()方法就能获取到解析的数据。

internal fun discoverAndInitialize(context: Context): StartupProviderStore {
 
    TraceCompat.beginSection(StartupInitializer::class.java.simpleName)
 
    val result = mutableListOf<AndroidStartup<*>>()
    val initialize = mutableListOf<String>()
    val initialized = mutableListOf<String>()
    var config: StartupProviderConfig? = null
    try {
        val provider = ComponentName(context.packageName, StartupProvider::class.java.name)
        val providerInfo = context.packageManager.getProviderInfo(provider, PackageManager.GET_META_DATA)
        val startup = context.getString(R.string.android_startup)
        val providerConfig = context.getString(R.string.android_startup_provider_config)
        providerInfo.metaData?.let { metaData ->
            metaData.keySet().forEach { key ->
                val value = metaData[key]
                val clazz = Class.forName(key)
                if (startup == value) {
                    if (AndroidStartup::class.java.isAssignableFrom(clazz)) {
                        doInitialize((clazz.getDeclaredConstructor().newInstance() as AndroidStartup<*>), result, initialize, initialized)
                    }
                } else if (providerConfig == value) {
                    if (StartupProviderConfig::class.java.isAssignableFrom(clazz)) {
                        config = clazz.getDeclaredConstructor().newInstance() as? StartupProviderConfig
                        // save initialized config
                        StartupCacheManager.instance.saveConfig(config?.getConfig())
                    }
                }
            }
        }
    } catch (t: Throwable) {
        throw StartupException(t)
    }
 
    TraceCompat.endSection()
 
    return StartupProviderStore(result, config)
}

核心逻辑是:

  1. 通过ComponentName()获取指定的StartupProvider
  2. 通过getProviderInfo()获取对应StartupProvider下的meta-data数据
  3. 遍历meta-data数组
  4. 根据事先预定的value来匹配对应的name
  5. 最终通过反射来获取对应name的实例

其中在解析Statup的过程中,为了减少Statup的配置,使用doInitialize()方法来自动创建依赖的Startup,并且提前对循环依赖进行检查。

依赖支持

/**
 * Returns a list of the other [Startup] objects that the initializer depends on.
 */
fun dependencies(): List<Class<out Startup<*>>>?

/**
 * Called whenever there is a dependency completion.
 *
 * @param [startup] dependencies [startup].
 * @param [result] of dependencies startup.
 */
fun onDependenciesCompleted(startup: Startup<*>, result: Any?)

某个初始化的组件在初始化之前所依赖的组件都必须通过dependencies()进行申明。申明之后会在后续进行解析,保证依赖的组件优先执行完毕;同时依赖的组件执行完毕会回调onDependenciesCompleted()方法。执行顺序则是通过有向图拓扑排序决定的。

闭环处理

有关闭环的处理,一方面会在自动配置环节的doInitialize()方法中会进行处理

private fun doInitialize(
    startup: AndroidStartup<*>,
    result: MutableList<AndroidStartup<*>>,
    initialize: MutableList<String>,
    initialized: MutableList<String>
) {
    try {
        val uniqueKey = startup::class.java.getUniqueKey()
        if (initialize.contains(uniqueKey)) {
            throw IllegalStateException("have circle dependencies.")
        }
        if (!initialized.contains(uniqueKey)) {
            initialize.add(uniqueKey)
            result.add(startup)
            startup.dependencies()?.forEach {
                doInitialize(it.getDeclaredConstructor().newInstance() as AndroidStartup<*>, result, initialize, initialized)
            }
            initialize.remove(uniqueKey)
            initialized.add(uniqueKey)
        }
    } catch (t: Throwable) {
        throw StartupException(t)
    }
}

将当前Startup加入到initialize中,同时遍历dependencies()依赖数组,递归调用doInitialize()

在递归的过程中,如果在initialize中存在对应的uniqueKey(这里为Startup的唯一标识)则代表发送的互相依赖,即存在依赖环。

另一方面,再后续的有向图拓扑排序优化也会进行环处理

fun sort(startupList: List<Startup<*>>): StartupSortStore {
    ...
 
    if (mainResult.size + ioResult.size != startupList.size) {
        throw StartupException("lack of dependencies or have circle dependencies.")
    }

}

在排序优化过程中会将在主线程执行与非主线程执行的Startup进行分类,再分类过程中并不会进行排重处理,只关注当前的Startup是否再主线程执行。所以最后只要这两种分类的大小之和不等于Startup的总和就代表存在环,即有互相依赖。

线程处理

线程方面,使用的是StartupExecutor接口, 在AndroidStartup默认实现了它的接口方法createExecutor()

override fun createExecutor(): Executor = ExecutorManager.instance.ioExecutor

ExecutorManager中提供了三种线程分别为

  1. cpuExecutor: cpu使用频率高,高速计算;核心线程池的大小与cpu核数相关。
  2. ioExecutor: io操作,网络处理等;内部使用缓存线程池。
  3. mainExecutor: 主线程。

所以如果需要修改默认线程,可以重写createExecutor()方法。

异步等待

在上面的依赖支持部分已经提到使用dependencies()来设置依赖的组件。每一个初始化组件能够执行的前提是它自身的依赖组件全部已经执行完毕。

如果是同步依赖,自然很简单,只需要按照依赖的顺序依次执行即可。而对于异步依赖任务,则需要保证所有的异步依赖任务完成,当前组件才能正常执行。

Android Startup借助了CountDownLatch来保证异步依赖的执行完成监听。

CountDownLatch字面意思就是倒计时锁,它是作用于线程中,初始化时会设置一个count大小的倒计时,通过await()来等待倒计时的结束,只不过倒计时的数值减少是通过手动调用countDown()来触发的。

所以在抽象类AndroidStartup中,通过await()countDown()来保证异步任务的准确执行。

abstract class AndroidStartup<T> : Startup<T> {
 
    private val mWaitCountDown by lazy { CountDownLatch(dependencies()?.size ?: 0) }
    private val mObservers by lazy { mutableListOf<Dispatcher>() }
 
    override fun toWait() {
        try {
            mWaitCountDown.await()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
 
    override fun toNotify() {
        mWaitCountDown.countDown()
    }
    ...
}

我们通过toWait()方法来等待依赖组件的执行完毕,而依赖的组件任务执行完毕之后,通过toNotify()来通知当前组件,一旦所有的依赖执行完毕之后,就会释放当前的线程,使它继续执行下去。

toWait()toNotify()的具体调用时机分别在StartupRunnableStartupManagerDispatcher中执行。

依赖回调

在依赖回调之前,先来认识一个接口ManagerDispatcher

interface ManagerDispatcher {
 
    /**
     * dispatch prepare
     */
    fun prepare()
 
    /**
     * dispatch startup to executing.
     */
    fun dispatch(startup: Startup<*>, sortStore: StartupSortStore)
 
    /**
     * notify children when dependency startup completed.
     */
    fun notifyChildren(dependencyParent: Startup<*>, result: Any?, sortStore: StartupSortStore)
}

ManagerDispatcher中有三个接口方法,分别用来管理Startup的执行逻辑,保证执行前的准备工作,执行过程中的分发与执行后的回调。所以依赖回调自然也在其中。

调用逻辑被封装到notifyChildren()方法中。最终调用StartuponDependenciesCompleted()方法。

所以我们可以在初始化组件中重写onDependenciesCompleted()方法,从而拿到所依赖的组件完成后返回的结果。例如Sample中的SampleSyncFourStartup

class SampleSyncFourStartup: AndroidStartup<String>() {
 
    private var mResult: String? = null
 
    override fun create(context: Context): String? {
        return "$mResult + sync four"
    }
 
    override fun callCreateOnMainThread(): Boolean = true
 
    override fun waitOnMainThread(): Boolean = false
 
    override fun dependencies(): List<Class<out Startup<*>>>? {
        return listOf(SampleAsyncTwoStartup::class.java)
    }
 
    override fun onDependenciesCompleted(startup: Startup<*>, result: Any?) {
        mResult = result as? String?
    }
}

当然这是在当前组件中获取依赖组件的返回结果,Android Startup还提供了在任意时候来查询任意组件的执行状况,并且支持获取任意已经完成的组件的返回结果。

Android Startup提供StartupCacheManager来实现这些功能。具体使用方式可以通过查看Sample来获取。

手动通知

上面介绍了依赖回调,它是自动调用依赖完成后的一系列操作。Android Startup也提供了手动通知依赖任务的完成。

手动通知的设置是通过manualDispatch()方法开启。它将配合onDispatch()一起完成。

ManagerDispatcher接口具体实现类的notifyChildren()方法中,如果开启手动通知,就不会走自动通知流程,调用toNotify()方法,而是会将当前组件的Dispatcher添加到注册表中。等待onDispatche()的手动调用去唤醒toNotify()的执行。

override fun notifyChildren(dependencyParent: Startup<*>, result: Any?, sortStore: StartupSortStore) {
    // immediately notify main thread,Unblock the main thread.
    if (dependencyParent.waitOnMainThread()) {
        needAwaitCount.incrementAndGet()
        awaitCountDownLatch?.countDown()
    }
 
     sortStore.startupChildrenMap[dependencyParent::class.java.getUniqueKey()]?.forEach {
        sortStore.startupMap[it]?.run {
            onDependenciesCompleted(dependencyParent, result)
 
            if (dependencyParent.manualDispatch()) {
                dependencyParent.registerDispatcher(this)
            } else {
                toNotify()
            }
        }
    }
    ...
}

具体实现示例可以查看SampleManualDispatchStartup

拓扑优化

Android Startup中初始化组件与组件间的关系其实就是一张有向无环拓扑图

Sample中的一个demo为例:

android_startup_diagram.png

我们将每一个Startup的边指向目标为一个入度。根据这个规定很容易算出这四个Startup的入度

  1. SampleFirstStartup: 0
  2. SampleSecondStartup: 1
  3. SampleThirdStartup: 2
  4. SampleFourthStartup: 3

那么这个入度有什么用呢?根据由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。

  1. 选择一个入度为0的顶点并输出之;
  2. 从网中删除此顶点及所有出边

循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。

根据上面的步骤,可以得出上面的四个Startup的输出顺序为

SampleFirstStartup -> SampleSecondStartup -> SampleThirdStartup -> SampleFourthStartup

以上的输出顺序也是初始化组件间的执行顺序。这样即保证了依赖组件间的正常执行,也保证了初始化组件的执行顺序的最优解,即依赖组件间的等候时间最短,同时也检查了依赖组件间是否存在环。

既然已经有了方案与实现步骤,下面要做的就是用代码实现出来。

fun sort(startupList: List<Startup<*>>): StartupSortStore {
    TraceCompat.beginSection(TopologySort::class.java.simpleName)

    val mainResult = mutableListOf<Startup<*>>()
    val ioResult = mutableListOf<Startup<*>>()
    val temp = mutableListOf<Startup<*>>()
    val startupMap = hashMapOf<String, Startup<*>>()
    val zeroDeque = ArrayDeque<String>()
    val startupChildrenMap = hashMapOf<String, MutableList<String>>()
    val inDegreeMap = hashMapOf<String, Int>()

    startupList.forEach {
        val uniqueKey = it::class.java.getUniqueKey()
        if (!startupMap.containsKey(uniqueKey)) {
            startupMap[uniqueKey] = it
            // save in-degree
            inDegreeMap[uniqueKey] = it.dependencies()?.size ?: 0
            if (it.dependencies().isNullOrEmpty()) {
                zeroDeque.offer(uniqueKey)
            } else {
                // add key parent, value list children
                it.dependencies()?.forEach { parent ->
                    val parentUniqueKey = parent.getUniqueKey()
                    if (startupChildrenMap[parentUniqueKey] == null) {
                        startupChildrenMap[parentUniqueKey] = arrayListOf()
                    }
                    startupChildrenMap[parentUniqueKey]?.add(uniqueKey)
                }
            }
        } else {
            throw StartupException("$it multiple add.")
        }
    }

    while (!zeroDeque.isEmpty()) {
        zeroDeque.poll()?.let {
            startupMap[it]?.let { androidStartup ->
                temp.add(androidStartup)
                // add zero in-degree to result list
                if (androidStartup.callCreateOnMainThread()) {
                    mainResult.add(androidStartup)
                } else {
                    ioResult.add(androidStartup)
                }
            }
            startupChildrenMap[it]?.forEach { children ->
                inDegreeMap[children] = inDegreeMap[children]?.minus(1) ?: 0
                // add zero in-degree to deque
                if (inDegreeMap[children] == 0) {
                    zeroDeque.offer(children)
                }
            }
        }
    }

    if (mainResult.size + ioResult.size != startupList.size) {
        throw StartupException("lack of dependencies or have circle dependencies.")
    }

    val result = mutableListOf<Startup<*>>().apply {
        addAll(ioResult)
        addAll(mainResult)
    }
    printResult(temp)

    TraceCompat.endSection()

    return StartupSortStore(
        result,
        startupMap,
        startupChildrenMap
    )
}

有了上面的步骤,相信这段代码都能够理解。

除了上面所介绍的功能,Android Startup还支持Systrace插桩,为用户提供系统分析初始化的耗时详细过程;初始化组件的准确耗时收集统计,方便用户下载与上传到指定服务器等等。

Android Startup的核心功能分析暂时就到这里结束了,希望能够对你有所帮助。

当然,本人真诚的邀请你加入Android Startup的建设中,如果你有什么好的建议也请不吝赐教。

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

为自己代言

微信公众号:【Android补给站】或者扫描下方二维码进行关注

Android补给站.jpg

查看原文

赞 3 收藏 3 评论 0

午后一小憩 发布了文章 · 8月5日

我为何弃用Jetpack的App Startup?

2020_08_05_00_07.jpeg

前言

最近Jetpack又添加了新成员App Startup,官方声明这是一个在Android应用启动时,针对初始化组件进行优化的依赖库。本人第一次听到后非常高兴,因为自己负责的项目在启动时需要初始化的东西实在是太多,而且有点杂乱无章,都耦合在一起了。对于可以异步初始化的组件也没有进行异步处理,而对于已经处理过的异步组件它们之间的依赖关系或者多个异步之后的统一逻辑处理也没有一个很好的统一规范。所以针对这种情况早就想找个方案来优化了,这次终于等到了App Startup

但是,当我元气满满的去查看官方文档时,并没有找到预想中的结果。官方文档中只提到了可以通过一个ContentProvider来统一管理需要初始化的组件,同时通过dependencies()方法解决组件间初始化的依赖顺序,然后呢?没了?等等官方你是不是漏了什么?

异步处理呢?虽然我们可以在create()方法中手动创建子线程进行异步任务,但一个异步任务依赖另一个异步任务又该如何处理呢?多个异步任务完成之后,统一逻辑处理又在哪里呢?依赖任务完成后的回调又在哪里?亦或者是依赖任务完成后的通知?

我有点不相信,所以又去查看了App Startup的源码,源码很简单,也就几个文件,最后发现确实只支持上面的那几个功能。

如果你的项目都是同步初始化的话,并且使用到了多个ContentProviderApp Startup可能有一定的优化空间,毕竟统一到了一个ContentProvider中,同时支持了简单的顺序依赖。

值得一提的是,App Startup中只提供了使用反射来获取初始化的组件实例,这对于一些没有过多依赖的初始化项目来说,盲目使用App Startup来优化是否会对启动速度进一步造成影响呢?

所以细想了一下,不禁让我想起了三国时的一个名词:鸡肋。食之无味,弃之可惜。

但最终我还是决定放弃使用它。

放弃之后有点不甘心,可能更多的是它没有解决我当前的项目场景。都分析了这么多,源码都看了,总不能半途而废吧,所以自己咬咬牙再补充一点呗。

所以坚持一下,就有了下面这个库,App Startup的进阶版Android Startup

Android Startup

Android Startup提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用Android Startup来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。
与此同时,Android Startup支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。

由于Android Startup是基于App Startup进行的扩展,所以它的使用方式与App Startup有点类似,该有的功能基本上都有,同时额外还附加其它功能。

下面是一张与google的App Startup功能对比的表格。

指标App StartupAndroid Startup
手动配置
自动配置
依赖支持
闭环处理
线程控制
异步等待
依赖回调
拓扑优化

下面简单介绍一下Android Startup的使用。

添加依赖

将下面的依赖添加到build.gradle文件中:

dependencies {
    implementation 'com.rousetime.android:android-startup:1.0.1'
}
依赖版本的更新信息: Release

快速使用

android-startup提供了两种使用方式,在使用之前需要先定义初始化的组件。

定义初始化的组件

每一个初始化的组件都需要实现AndroidStartup<T>抽象类,它实现了Startup<T>接口,它主要有以下四个抽象方法:

  • callCreateOnMainThread(): Boolean用来控制create()方法调时所在的线程,返回true代表在主线程执行。
  • waitOnMainThread(): Boolean用来控制当前初始化的组件是否需要在主线程进行等待其完成。如果返回true,将在主线程等待,并且阻塞主线程。
  • create(): T?组件初始化方法,执行需要处理的初始化逻辑,支持返回一个T类型的实例。
  • dependencies(): List<Class<out Startup<*>>>?返回Startup<*>类型的list集合。用来表示当前组件在执行之前需要依赖的组件。

例如,下面定义一个SampleFirstStartup类来实现AndroidStartup<String>抽象类:

class SampleFirstStartup : AndroidStartup<String>() {
 
    override fun callCreateOnMainThread(): Boolean = true
 
    override fun waitOnMainThread(): Boolean = false
 
    override fun create(context: Context): String? {
        // todo something
        return this.javaClass.simpleName
    }
 
    override fun dependencies(): List<Class<out Startup<*>>>? {
        return null
    }
 
}

因为SampleFirstStartup在执行之前不需要依赖其它组件,所以它的dependencies()方法可以返回空,同时它会在主线程中执行。

注意:️虽然waitOnMainThread()返回了false,但由于它是在主线程中执行,而主线程默认是阻塞的,所以callCreateOnMainThread()返回true时,该方法设置将失效。

假设你还需要定义SampleSecondStartup,它依赖于SampleFirstStartup。这意味着在执行SampleSecondStartup之前SampleFirstStartup必须先执行完毕。

class SampleSecondStartup : AndroidStartup<Boolean>() {
 
    override fun callCreateOnMainThread(): Boolean = false
 
    override fun waitOnMainThread(): Boolean = true
 
    override fun create(context: Context): Boolean {
        // 模仿执行耗时
        Thread.sleep(5000)
        return true
    }
 
    override fun dependencies(): List<Class<out Startup<*>>>? {
        return listOf(SampleFirstStartup::class.java)
    }
 
}

dependencies()方法中返回了SampleFirstStartup,所以它能保证SampleFirstStartup优先执行完毕。
它会在子线程中执行,但由于waitOnMainThread()返回了true,所以主线程会阻塞等待直到它执行完毕。

例如,你还定义了SampleThirdStartupSampleFourthStartup

Manifest中自动配置

第一种初始化方法是在Manifest中进行自动配置。

在Android Startup中提供了StartupProvider类,它是一个特殊的content provider,提供自动识别在manifest中配置的初始化组件。
为了让其能够自动识别,需要在StartupProvider中定义<meta-data>标签。其中的name为定义的组件类,value的值对应为android.startup

<provider
    android:name="com.rousetime.android_startup.provider.StartupProvider"
    android:authorities="${applicationId}.android_startup"
    android:exported="false">
 
    <meta-data
         android:name="com.rousetime.sample.startup.SampleFourthStartup"
        android:value="android.startup" />
 
</provider>

你不需要将SampleFirstStartupSampleSecondStartupSampleThirdStartup添加到<meta-data>标签中。这是因为在SampleFourthStartup中,它的dependencies()中依赖了这些组件。StartupProvider会自动识别已经声明的组件中依赖的其它组件。

Application中手动配置

第二种初始化方法是在Application进行手动配置。

手动初始化需要使用到StartupManager.Builder()

例如,如下代码使用StartupManager.Builder()进行初始化配置。

class SampleApplication : Application() {
 
    override fun onCreate() {
        super.onCreate()
        StartupManager.Builder()
            .addStartup(SampleFirstStartup())
            .addStartup(SampleSecondStartup())
            .addStartup(SampleThirdStartup())
            .addStartup(SampleFourthStartup())
            .build(this)
            .start()
            .await()
    }
}

如果你开启了日志输出,然后运行项目之后,将会在控制台中输出经过拓扑排序优化之后的初始化组件的执行顺序。

 D/StartupTrack: TopologySort result: 
    ================================================ ordering start ================================================
    order [0] Class: SampleFirstStartup => Dependencies size: 0 => callCreateOnMainThread: true => waitOnMainThread: false
    order [1] Class: SampleSecondStartup => Dependencies size: 1 => callCreateOnMainThread: false => waitOnMainThread: true
    order [2] Class: SampleThirdStartup => Dependencies size: 2 => callCreateOnMainThread: false => waitOnMainThread: false
    order [3] Class: SampleFourthStartup => Dependencies size: 3 => callCreateOnMainThread: false => waitOnMainThread: false
    ================================================ ordering end ================================================

完整的代码实例,你可以通过查看app获取。

更多

可选配置

  • LoggerLevel: 控制Android Startup中的日志输出,可选值包括LoggerLevel.NONE, LoggerLevel.ERROR and LoggerLevel.DEBUG
  • AwaitTimeout: 控制Android Startup中主线程的超时等待时间,即阻塞的最长时间。

Manifest中配置

使用这些配置,你需要定义一个类去实现StartupProviderConfig接口,并且实现它的对应方法。

class SampleStartupProviderConfig : StartupProviderConfig {
 
    override fun getConfig(): StartupConfig =
        StartupConfig.Builder()
            .setLoggerLevel(LoggerLevel.DEBUG)
            .setAwaitTimeout(12000L)
            .build()
}

与此同时,你还需要在manifest中进行配置StartupProviderConfig

<provider
     android:name="com.rousetime.android_startup.provider.StartupProvider"
    android:authorities="${applicationId}.android_startup"
    android:exported="false">
 
    <meta-data
         android:name="com.rousetime.sample.startup.SampleStartupProviderConfig"
         android:value="android.startup.provider.config" />
 
</provider>

经过上面的配置,StartupProvider会自动解析SampleStartupProviderConfig

Application中配置

在Application需要借助StartupManager.Builder()进行配置。

override fun onCreate() {
    super.onCreate()
 
    val config = StartupConfig.Builder()
        .setLoggerLevel(LoggerLevel.DEBUG)
        .setAwaitTimeout(12000L)
        .build()
 
    StartupManager.Builder()
        .setConfig(config)
        ...
        .build(this)
        .start()
        .await()
}

方法

AndroidStartup

  • createExecutor(): Executor: 如果定义的组件没有运行在主线程,那么可以通过该方法进行控制运行的子线程。
  • onDependenciesCompleted(startup: Startup<*>, result: Any?): 该方法会在每一个依赖执行完毕之后进行回调。

实战测试

AwesomeGithub中使用了Android Startup,优化配置的初始化时间与组件化开发的配置注入时机,使用前与使用后时间对比:

状态启动页面消耗时间
使用前WelcomeActivity420ms
使用后WelcomeActivity333ms

AwesomeGithub

AwesomeGithub是基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

项目图

除了Android原生版本,还有基于Flutter的跨平台版本flutter_github

如果你喜欢我的文章,你可以关注我的微信公众号:【Android补给站】或者扫描下方二维码进行关注,当然你也可以直接关注当前网站的帐号。主要区别就是微信能够更方法互动。

公众号更新不会很频繁,但一旦更新必定是纯干货。

Android补给站.jpg

查看原文

赞 2 收藏 2 评论 0

午后一小憩 发布了文章 · 7月17日

Android Hilt实战初体验: Dagger替换成Hilt

2020_07_16_00_05.png

在组件化AwesomeGithub项目中使用了Dagger来减少手动依赖注入代码。虽然它能自动化帮我们管理依赖项,但是写过之后的应该都会体会到它还是有点繁琐的。项目中到处充斥着Component,这让我想起了传统MVP模式的接口定义。

简单来说就是费劲,有许多大量的类似定义。可能google也意识到这一点了,所以前不久发布出了Hilt

Hilt

为了防止没听说过的小伙伴们一头雾水,首先我们来了解下Hilt是什么?

HiltAndroid的依赖注入库,可减少在项目中执行手动依赖项注入的样板代码。

Hilt通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。HiltDagger 的基础上构建而成,因而能够具有 Dagger 的编译时正确性、运行时性能、可伸缩性。

那么有的小伙伴可能会有疑问,既然已经有了Dagger那为什么还要Hilt的呢?

HiltDagger的主要目标都是一致的:

  1. 简化 Android 应用的 Dagger 相关基础架构。
  2. 创建一组标准的组件和作用域,以简化设置、提高可读性以及在应用之间共享代码。
  3. 提供一种简单的方法来为各种构建类型(如测试、调试或发布)配置不同的绑定。

但是Android中会实例化许多组件类,例如Activity,因此在应用中使用Dagger需要开发者编写大量的样板代码。Hilt可以减少这些样板代码。

Hilt做的优化包括

  1. 无需编写大量的Component代码
  2. Scope也会与Component自动绑定
  3. 预定义绑定,例如 ApplicationActivity
  4. 预定义的限定符,例如@ApplicationContext@ActivityContext

下面通过AwesomeGithubDagger来对比了解Hilt的具体使用。

依赖

使用之前将Hilt的依赖添加到项目中。

首先,将hilt-android-gradle-plugin插件添加到项目的根级 build.gradle文件中:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

然后,应用Gradle插件并在app/build.gradle文件中添加以下依赖项:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
 
android {
    ...
}
 
dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Application类

使用Dagger时,需要一个AppComponent单例组件,项目中的其它SubComponent都将依赖于它,所以在AwesomeGithub中它大概是这个样子

@Singleton
@Component(
    modules = [
        SubComponentModule::class,
        NetworkModule::class,
        ViewModelBuilderModule::class
    ]
)
interface AppComponent {
 
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): AppComponent
    }
 
    fun welcomeComponent(): WelcomeComponent.Factory
 
    fun mainComponent(): MainComponent.Factory
     
    ...
 
    fun loginComponent(): LoginComponent.Factory
 
}
 
@Module(
    subcomponents = [
        WelcomeComponent::class,
        MainComponent::class,
        ...
        LoginComponent::class
    ]
)
object SubComponentModule

上面的我已经省略了大半,是不是看起来很多,而且最重要的是很多重复的结构基本都是一样的。所以Hilt基于这一点进行了简化,将这些重复的编写转成构建的时候自动生成。

Hilt要做的很简单,添加几个注释

@HiltAndroidApp
class App : Application() { ... }

所有的Hilt应用都必须包含一个带@HiltAndroidApp注释的Application。它将替代Dagger中的AppComponent

Android类

对于Android类,使用Dagger时需要定义SubComponent并将它依赖到Application类中。下面以WelcomeActivity为例。

@Subcomponent(modules = [WelcomeModule::class])
interface WelcomeComponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(): WelcomeComponent
    }

    fun inject(activity: WelcomeActivity)
}
module的部分先不说,后面会提及

下面看Hilt的实现

@AndroidEntryPoint
class MainActivity : BaseHiltActivity<ActivityMainBinding, MainVM>() { ... }

Hilt要做的是添加@AndroidEntryPoint注释即可。

惊讶,结合上面的,两个注解就替换了Dagger的实现,现在是否体会到Hilt的简洁?对新手来说也可以降低很大的学习成本。

目前Hilt支持下面Android

  1. Application (@HiltAndroidApp)
  2. Activity
  3. Fragment
  4. View
  5. Searvice
  6. BroadcastReceiver

有一点需要注意,如果使用@AndroidEntryPoint注释了某个类,那么依赖该类的其它类也需要添加。

典型的就是Fragment,所以除了Fragment还需要给依赖它的所有Activity进行注释。

@AndroidEntryPoint的作用,对照一下Dagger就知道了。它会自动帮我们生成对应Android类的Componennt,并将其添加到Application类中。

@Inject

@Inject的使用基本与Dagger一致,可以用来定义构造方法或者字段,声明该构造方法或者字段需要通过依赖获取。

class UserRepository @Inject constructor(
    private val service: GithubService
) : BaseRepository() { ... }

@Module

Hilt模块也需要添加@Module注释,与Dagger不同的是它还必须使用@InstallIn为模块添加注释。目的是告知模块用在哪个Android类中。

@Binds

@Binds注释会告知Hilt在需要提供接口的实例时要使用哪种实现。
它的用法与Dagger没什么区别

@Module
@InstallIn(ActivityComponent::class)
abstract class WelcomeModule {
 
    @Binds
    @IntoMap
    @ViewModelKey(WelcomeVM::class)
    abstract fun bindViewModel(viewModel: WelcomeVM): ViewModel
}

不同的是需要添加@InstallInActivityComponent::class用来表明该模块作用范围为Activity

其实上面这块对ViewModel的注入,使用Hilt时会自动帮我们编写,这里只是为了展示与Dagger的不同之处。后续会提到ViewModel的注入。

@Providers

提供一个FragmentManager的实例,首先是Dagger的使用

@Module
class MainProviderModule(private val activity: FragmentActivity) {
 
    @Provides
    fun providersFragmentManager(): FragmentManager = activity.supportFragmentManager
}

对比一下Hilt

@InstallIn(ActivityComponent::class)
@Module
object MainProviderModule {
 
    @Provides
    fun providerFragmentManager(@ActivityContext context: Context) = (context as FragmentActivity).supportFragmentManager
}

区别是在Hilt@Providers必须为static类并且构造方法不能有参数。

@ActivityContextHilt提供的预定限定符,它能提供来自与ActivityContext,对应的还有@ApplicationContext

提供的组件

对于之前提到的@InstallIn会关联不同的Android类,除了@ActivityComponent还有以下几种

hilt_1.png

对应的生命周期如下

hilt_2.png

同时还提供了相应的作用域

hilt_3.png

所以Hilt的默认提供将大幅提高开发效率,减少许多重复的定义

ViewModel

最后再来说下ViewModel的注入。如果你使用到了Jetpack相信少不了它的注入。

对于Dagger我们需要自定义一个ViewModelFactory,并且提供注入方式,例如在AwesomeGithubcomponentbridget模块中定义了ViewModelFactory

@Module
abstract class ViewModelBuilderModule {
 
    @Binds
    abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
  
}
 
class ViewModelFactory @Inject constructor(private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        var creator = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                }
            }
        }
 
        if (creator == null) {
            throw IllegalArgumentException("Unknown model class: $modelClass")
        }
 
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException()
        }
    }
 
}

通过@Inject来注入构造实例,但构造方法中需要提供Map类型的creators。这个时候可以使用@IntoMap,为了匹配Map的类型,需要定义一个@MapKey的注释

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

然后再到对应的组件下使用,例如匹配MainVM

@Module
abstract class MainModule {
 
    @Binds
    @IntoMap
    @ViewModelKey(MainVM::class)
    abstract fun bindViewModel(viewModel: MainVM): ViewModel
 
}

这样就提供了Map<Class<MainVM>, MainVM>的参数类型,这时我们自定义的ViewModelFactory就能够被成功注入。

例如basic模块里面的BaseDaggerActivity

abstract class BaseDaggerActivity<V : ViewDataBinding, M : BaseVM> : AppCompatActivity() {

    protected lateinit var viewDataBinding: V

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    protected val viewModel by lazy { ViewModelProvider(this, viewModelFactory)[getViewModelClass()] }
    ...
}

当然,别忘了MainVM也需要使用@Inject来声明注入

class MainVM @Inject constructor() : BaseVM() { ... }

以上是DaggerViewModel使用的注入方式。

虽然自定义的ViewModelFactory是公用的,但是对于不同的ViewModel还是要手动定义不同的bindViewModel方法。

而对于Hilt却可以省略这一步,甚至说上面的全部都不需要手动编写。我们需要做的是只需在ViewModel的构造函数上添加@ViewModelInject

例如上面的MainVM,使用Hilt的效果如下

class MainVM @ViewModelInject constructor() : BaseVM() { ... }

至于Hilt为什么会这么简单呢?我们不要忘了它的本质,它是在Dagger之上建立的,本质是为了帮助我们减少不必要的样板模板,方便开发者更好的使用依赖注入。

Hilt中,上面的实现会自动帮我们生成,所以才会使用起来这么简单。

如果你去对比看AwesomeGithub上的feat_daggerfeat_hilt两个分支中的代码,就会发现使用Hilt明显少了许多代码。对于简单的Android类来说就是增加几个注释而已。

目前唯一一个比较不理想的是对于@Providers的使用,构造方法中不能有参数,如果在用Dagger使用时已经有参数了,再转变成Hilt可能不会那么容易。

庆幸的是,DaggerHilt可以共存。所以你可以选择性的使用。

但是整体而言Hilt真香,你只要尝试了绝不会后悔~

AwesomeGithub

AwesomeGithub是基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

awesome_github.png

除了Android原生版本,还有基于Flutter的跨平台版本flutter_github

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,你可以关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时能够更方便的收到相关的技术推送。

Android补给站.jpg

查看原文

赞 6 收藏 4 评论 0

午后一小憩 发布了文章 · 7月13日

从零开始的Flutter之旅: MethodChannel

2020_07_09_18_50.jpeg

往期回顾

从零开始的Flutter之旅: StatelessWidget

从零开始的Flutter之旅: StatefulWidget

从零开始的Flutter之旅: InheritedWidget

从零开始的Flutter之旅: Provider

从零开始的Flutter之旅: Navigator

flutter_github有这么一个场景:通过authorization认证方式进行登录。而authorization的具体登录形式是,通过跳转一个网页链接进行github授权登录,成功之后会携带对应的code到指定客户端中,然后客户端可以通过这个code来进行oauth授权登录,成功之后客户端可以拿到该账户的token,所以之后的github操作都可以通过该token来进行请求。由于token是有时效性,同时也可以手动解除授权,所以相对于在客户端进行账户密码登录来说更加安全。

method_channel.gif

那么要实现上面这个场景,Flutter就需要与原生客户端进行通信,拿到返回的code,然后再到Flutter中进行oauth授权登录请求。

通信方式可以使用MethodChannel,这个就是今天的主题。

OAuth App

authorization认证的原理已经知道了,下面直接来看实现方案。

首先我们需要一个OAuth App用来提供用户通过github授权的应用。

这个在github上可以直接注册的

method_channel_1.png

在注册的OAuth App时会有一个Authorization callback URL必填项。这个callback url的作用就是当你通过该链接认证通过后会以App Link的方式使用该url跳转到对应的App应用,同时返回认证成功的code。这里将其定义为REDIRECT_URI

注册成功之后,我们拿到它的Client IDClient SecretAuthorization callback URL,拼接成下面的连接

const String URL_AUTHORIZATION =
    'https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=user%20repo%20notifications%20';

有了跳转到外部的认证链接之后,下面就是在应用中实现这个跳转认证流程。

url_launcher

首先需要跳转外部浏览器访问上面的authorization链接。这一步的实现需要借助url_launcher,它能够帮助我们检查链接是否有效,同时启动外部浏览器进行跳转。

在使用之前需要在pubspec.yaml中添加依赖

dependencies:
  flutter:
    sdk: flutter
  http: 0.12.0+4
  dio: 3.0.7
  shared_preferences: 0.5.6+1
  url_launcher: 5.4.1
  ...

依赖成功之后,使用canLaunch()来检查链接的有效性;launch()来启动跳转

  authorization() {
    return () async {
      FocusScope.of(context).requestFocus(FocusNode());
      if (await canLaunch(URL_AUTHORIZATION)) {
        // 为设置forceSafariVC,IOS 默认会打开APP内部WebView
        // 而APP内部WebView不支持重定向跳转到APP
        await launch(URL_AUTHORIZATION, forceSafariVC: false);
      } else {
        throw 'Can not launch $URL_AUTHORIZATION)';
      }
    };
  }

Scheme

通过authorization()方法可以成功跳转到外部浏览器进行登录授认证。授权成功之后会返回到之前的app,具体页面路径与链接中配置的REDIRECT_URI有关。

const String REDIRECT_URI = 'github://login';

这里定义了一个Scheme,为了能够成功返回到客户端指定的页面,我们需要为Android与IOS配置对应的Scheme。

Android

找到AndroidManifest文件,在activity便签下添加intent-filter属性

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
 
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
 
    <data
        android:host="login"
        android:scheme="github" />
</intent-filter>

前面的actioncategory配置是固定的,如果需要支持不同的scheme,主要修改的是data中的配置。

schemehost分别对应到REDIRECT_URI中的数值。

IOS

找到info.plist文件,添加URL types便签,在它的item下配置对应的URL identifierURL Schemes

method_channel_2.png

配置完scheme之后,就能够正常返回到对应的客户端页面。

接下来需要考虑的是,如何拿到返回的code值

MethodChannel

这个时候今天的主角就该上场了。

MethodChannel简单的说就是Flutter提供与客户端通信的渠道,使用时互相约定一个渠道name与对应的调用客户端指定方法的method

所以我们先来约定好这两个值

const String METHOD_CHANNEL_NAME = 'app.channel.shared.data';
const String CALL_LOGIN_CODE = 'getLoginCode';

然后通过MethodChannel来获取对应的渠道

  callLoginCode(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      final platform = const MethodChannel(METHOD_CHANNEL_NAME);
      final code = await platform.invokeMethod(CALL_LOGIN_CODE);
      if (code != null) {
        _getAccessTokenFromCode(code);
      }
    }
  }

使用invokeMethod来调用客户端对应的方法,这里是用来获取授权成功后返回客户端的code。

这是Flutter调用客户端方法的步骤,下面再看客户端的实现

Android

首先我们将约定好的渠道名称与回调方法名定义为常量

object Constants {
    const val AUTHORIZATION_CODE = "code"
    const val METHOD_CHANNEL_NAME = "app.channel.shared.data"
    const val CALL_LOGIN_CODE = "getLoginCode"
}

在之前我们已经在AndroidManifest.xml中定义的scheme,所以认证成功后回返回客户端的MainActivity页面,同时回调onNewIntent方法。

所以获取返回code的方式可以在onNewIntent中进行,同时还需要建立对应的MethodChannel与提供回调的方法。具体实现如下:

class MainActivity : FlutterActivity() {
 
    private var mAuthorizationCode: String? = null
 
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        setupMethodChannel()
    }
 
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        getExtra(intent)
    }
 
    private fun getExtra(intent: Intent?) {
        // from author login
        mAuthorizationCode = intent?.data?.getQueryParameter(Constants.AUTHORIZATION_CODE)
    }
 
    private fun setupMethodChannel() {
        MethodChannel(flutterEngine?.dartExecutor, Constants.METHOD_CHANNEL_NAME).setMethodCallHandler { call, result ->
            if (call.method == Constants.CALL_LOGIN_CODE && !TextUtils.isEmpty(mAuthorizationCode)) {
                result.success(mAuthorizationCode)
                mAuthorizationCode = null
            }
        }
    }
}

MethodChannel建立渠道,setMethodCallHandler来响应Flutter中需要调用的方法。通过判断回调的方法名称,即之前在Flutter中约定的CALL_LOGIN_CODE。来执行对应的逻辑

因为我们需要返回的code值,只需通过resultsuccess方法,将获取到的code传递过去即可。之后Flutter就能够获取到该值。

IOS

AppDelegate.swift中定义一个methodChannel,使用约定好的name。

methodChannel的创建IOS是通过FlutterMethodChannel.init来生成。之后的回调与Android的基本类似

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    var paramsMap: Dictionary<String, String> = [:]
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
     
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let methodChannel = FlutterMethodChannel.init(name: "app.channel.shared.data", binaryMessenger: controller.binaryMessenger)
     
    methodChannel.setMethodCallHandler { (call, result) in
        if "getLoginCode" == call.method && !self.paramsMap.isEmpty {
            result(self.paramsMap["code"])
            self.paramsMap.removeAll()
        }
    }
     
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        let absoluteString = url.absoluteURL.absoluteString
        let urlComponents = NSURLComponents(string: absoluteString)
        let queryItems = urlComponents?.queryItems
        for item in queryItems! {
            paramsMap[item.name] = item.value
        }
        return true
    }
}

setMethodCallHandler中判断回调的方法是否与约定的方法名一致,如果一致再通过result方法将code传递给Flutter。

至此Android与IOS都与Flutter建立了通信,它们之间的桥梁就是通过MethodChannel来搭建的。

最后code传回到Flutter之后,我们再将code进行请求获取到对应的token。

到这里整个授权认证就完成了,之后我们就可以通过token来请求用户相关的接口,获取对应的数据。

token的获取与相关接口的调用可以通过查看flutter_github源码获取

flutter_github

flutter_github,这是一个基于Github Open Api开发的Flutter版本的Github客户端。该项目主要是用来练习Flutter,感兴趣的可以加入一起来学习,如果有帮助的话也请不要吝啬你的关注。

flutter_github_preview.png

当然如果你想了解Android原生,AwesomeGithub是一个不错的选择。它是flutter_github的纯Android版本。

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,你可以关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时能够更方便的收到相关的技术推送。

Android补给站.jpg

查看原文

赞 4 收藏 3 评论 0

午后一小憩 发布了文章 · 6月28日

从零开始的Flutter之旅: Navigator

2020_06_28_07_32_2.jpeg

往期回顾

从零开始的Flutter之旅: StatelessWidget

从零开始的Flutter之旅: StatefulWidget

从零开始的Flutter之旅: InheritedWidget

从零开始的Flutter之旅: Provider

这篇文章是从零开始系列的第五期,前面我们讲到了Widget与结合数据共享的Provider处理。

这次我们接着来了解一下路由导航Navigator的相关信息。

Flutter中的路由管理与原生开发类似,都会维护一个路由栈,通过push入栈打开一个新的页面,然后再通过pop出栈关闭老的页面。

示例

我们直接到 flutter_github中找个简单的实例。

  void _goToLogin() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String authorization = prefs.getString(SP_AUTHORIZATION);
    String token = prefs.getString(SP_ACCESS_TOKEN);
    if ((authorization != null && authorization.isNotEmpty) ||
        (token != null && token.isNotEmpty)) {
      Navigator.of(context).push(MaterialPageRoute(builder: (context) {
        return HomePage();
      }));
    } else {
      Navigator.of(context).push(MaterialPageRoute(builder: (context) {
        return LoginPage();
      }));
    }
  }

上面的方法是判断是否已经登录了。如果登录了通过过Navigator跳转到HomePage页面,否则跳转到LoginPage页面。

用法很简单通过push传递一个Route。这里对应的是MaterialPageRoute。它会提供一个builder方法,我们直接在builder中返回想要跳转的页面实例即可。

它继承于PageRoute,PageRoute是一个抽象类,它提供了路由切换时的过渡动画效果与相应的接口。而MaterialPageRoute通过这些接口来实现不同平台上对应风格的路由切换动画效果。例如:

  1. Android平台,push时页面会从屏幕底部滑动到顶部进入,pop时页面会从屏幕顶部滑动到屏幕底部退出。
  2. Ios平台,push时页面会从屏幕右侧滑动到屏幕左侧进入,pop时页面会从屏幕左侧滑动到屏幕右侧退出。
如果想自定义切换动画,可以仿照MaterialPageRoute,继承于PageRoute来实现。

Navigator

需要注意的是,push操作会返回一个Future,它是用来接收新的路由关闭时返回的数据。在Android中对应的就是startActivityForResult() 和 onActivityResult()API。

 @optionalTypeArgs
  Future<T> push<T extends Object>(Route<T> route) {
    assert(!_debugLocked);
    assert(() {
      _debugLocked = true;
      return true;
    }());
    ....
    ....
  }

对应的另一个是pop操作,出栈是可以向之前的页面传递数据,在Android中对应的就是setResult() Api

  @optionalTypeArgs
  bool pop<T extends Object>([ T result ]) {
    assert(!_debugLocked);
    assert(() {
      _debugLocked = true;
      return true;
    }());
    final Route<dynamic> route = _history.last;
    assert(route._navigator == this);
    bool debugPredictedWouldPop;
    ...
    ...
  }

除了上面两个常用的,还有下面几个特殊的操作

  1. pushReplacement: 将当前的路由页面进行替换成新的路由页面, 之前的路由将会失效。
  2. pushAndRemoveUntil: 加入一个新的路由,同时它接收一个判断条件,如果满足条件将会移除之前所有的路由。

这些都是根据特定场景使用,例如文章最开始的登录判断示例。这段判断代码其实在App启动时的引导页面中,所以不管最终跳转到哪个页面,最终这个引导页面都需要从路由中消失,所以这里就可以通过pushReplacement来开启新的路由页面。

     Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context){
        return HomePage();
      }));

传参

路由跳转页面自然少不了参数的传递,通过上面的方式进行路由跳转,传参也非常简单,可以直接通过实例类进行传参。

我这里以flutter_github中的WebViePage为例。

class WebViewPage extends BasePage<_WebViewState> {
  final String url;
  final String requestUrl;
  final String title;
 
  WebViewPage({@required this.title, this.url = '', this.requestUrl = ''});
 
  @override
  _WebViewState createBaseState() => _WebViewState(title, url, requestUrl);
}

上面是WebViewPage参数的接收,直接通过实例化进行参数传递

  contentTap(int index, BuildContext context) {
    NotificationModel item = _notifications[index];
    if(item.unread) _markThreadRead(index, context);
    Navigator.push(context, MaterialPageRoute(builder: (_) {
      return WebViewPage(
        title: item.subject?.title ?? '',
        requestUrl: item.subject?.url ?? '',
      );
    }));
  }

这里是通过点击文本跳转到WebViewPage页面,使用push操作来导航到WebViewPage页面,同时在实例化时将相应的参数传递过去。

以上是相对比较原始的方法进行参数传递,还有另一种

做个Android的朋友都知道在Activity页面跳转时可以同Intent进行参数传递,而接受页面也可以通过Intent来获取传递过来的参数。

在Flutter中也有类似的传参方式。我们可以通过MaterialPageRoute中的settings来构建一个arguments对象,将其传递到跳转的页面中。

将上面的代码进行改版

  contentTap(int index, BuildContext context) {
    NotificationModel item = _notifications[index];
    if (item.unread) _markThreadRead(index, context);
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (_) {
              return WebViewPage();
            },
            settings: RouteSettings(
                arguments: {WebViewPage.ARGS_TITLE: item.subject?.title ?? '', WebViewPage.ARGS_REQUEST_URL: item.subject?.url ?? ''})));
  }

这是参数传递,下面是WebViewPage中对参数的接收处理

    Map<String, String> arguments = ModalRoute.of(context).settings.arguments;
    _title = arguments[WebViewPage.ARGS_TITLE];
    _url = arguments[WebViewPage.ARGS_URL];
    vm.requestUrl = arguments[WebViewPage.ARGS_REQUEST_URL];

在接收页面参数是通过ModalRoute来获取的,获取到的arguments就是上面传递过来的参数map数据。

ModalRoute.of()内部运用的是context.dependOnInheritedWidgetOfExactType()

是不是有点眼熟?如果不记得的话推荐重新温习一遍从零开始的Flutter之旅: InheritedWidget

以上都是非命名路由,下面我们再来了解一下命名路由的使用与参数方式。

命名路由

命名路由,顾名思义通过提前注册好的名称来跳转到对应的页面。

首页我们需要注册一个路由表,约定好名称与页面的一一对应。

而路由表可以通过routes来定义

  final Map<String, WidgetBuilder> routes;

通过定义,应该很好理解。它是一个map,key代表路由名称,value代表具体的页面实例。

flutter_github中的GithubApp为例。

class _GithubAppState extends State<GithubApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Github',
      theme: ThemeData.light(),
      initialRoute: welcomeRoute.routeName,
      routes: {
        welcomeRoute.routeName: (BuildContext context) => WelcomePage(),
        loginRoute.routeName: (BuildContext context) => LoginPage(),
        homeRoute.routeName: (BuildContext context) => HomePage(),
        repositoryRoute.routeName: (BuildContext context) => RepositoryPage(),
        followersRoute.routeName: (BuildContext context) =>
            FollowersPage(followersRoute.pageType),
        followingRoute.routeName: (BuildContext context) =>
            FollowersPage(followingRoute.pageType),
        webViewRoute.routeName: (BuildContext context) => WebViewPage(),
      },
    );
  }
}

需要注意的有两点

  1. initialRoute是用来初始化路由页面,它接收的也是对应路由页面的注册名称
  2. routes就是注册的路由表,只需通过key、value的方式来注册对应的路由页面。

为了方便管理路由的跳转,这里使用了AppRoutes来统一管理路由的名称

class AppRoutes {
  final String routeName;
  final String pageTitle;
  final String pageType;
 
  const AppRoutes(this.routeName, {this.pageTitle, this.pageType});
}
 
class PageType {
  static const String followers = 'followers';
  static const String following = 'following';
}
 
const AppRoutes welcomeRoute = AppRoutes('/');
 
const AppRoutes loginRoute = AppRoutes('/login');
 
const AppRoutes homeRoute = AppRoutes('/home');
 
const AppRoutes repositoryRoute =
    AppRoutes('/repository', pageTitle: 'repository');
 
const AppRoutes followersRoute = AppRoutes('/followers',
    pageTitle: 'followers', pageType: PageType.followers);
const AppRoutes followingRoute = AppRoutes('/following',
    pageTitle: 'following', pageType: PageType.following);
 
const AppRoutes webViewRoute = AppRoutes('/webview', pageTitle: 'WebView');

现在我们已经注册好了需要跳转的页面路由,接下来使用命名路由的方式来替换之前介绍的路由方式。

  void _goToLogin() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String authorization = prefs.getString(SP_AUTHORIZATION);
    String token = prefs.getString(SP_ACCESS_TOKEN);
    if ((authorization != null && authorization.isNotEmpty) ||
        (token != null && token.isNotEmpty)) {
      Navigator.pushReplacementNamed(context, homeRoute.routeName);
    } else {
      Navigator.pushReplacementNamed(context, loginRoute.routeName);
    }
  }

在登录状态判断跳转的过程中,可以直接通过pushReplacementNamed()来跳转到对应的页面。与之前的区别是,我们只需传递对应跳转页面的路由名称。因为已经有了路由注册表,所以会自己转变成相应的页面。

对应的方法还有pushNamed()与pushNamedAndRemoveUntil()

对于命名路由的参数传递与之前最后面介绍的参数传递方式类似,例如

    Navigator.of(context).pushNamed(webViewRoute.routeName, 
        arguments: {WebViewPage.ARGS_TITLE: item.subject?.title ?? '', WebViewPage.ARGS_REQUEST_URL: item.subject?.url ?? ''});

基本上类似,也是传递一个arguments,对应的页面接收参数的方式保存不变。

onGenerateRoute

命名路由中还有一个需要注意的是onGenerateRoute

class _GithubAppState extends State<GithubApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
    ...
      onGenerateRoute: (RouteSettings setting) {
        return MaterialPageRoute(builder: (context) {
          String routeName = setting.name;
          // todo navigator
        });
      },
    );
  }
}

它的回调条件是:跳转的页面没有在routes中进行路由注册

通过该回调方法,我们可以在这里进行路由拦截,再统一做一些页面跳转的逻辑处理。

Navigator方面的知识就介绍到这里,如果文章中有不足的地方欢迎指出,或者说你这其中有什么疑问也可以留言与我,我将力所能及的进行解答。

推荐项目

下面介绍一个完整的Flutter项目,对于新手来说是个不错的入门。

flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

flutter_github_preview.png

当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

Android补给站.jpg

查看原文

赞 6 收藏 5 评论 0

午后一小憩 发布了文章 · 6月22日

从零开始的Flutter之旅: Provider

2020_06_22_19_24.jpeg

往期回顾

从零开始的Flutter之旅: StatelessWidget

从零开始的Flutter之旅: StatefulWidget

从零开始的Flutter之旅: InheritedWidget

在上篇文章中我们介绍了InheritedWidget,并在最后引发出一个问题。

虽然InheritedWidget可以提供共享数据,并且通过getElementForInheritedWidgetOfExactType来解除didChangeDependencies的调用,但还是没有避免CountWidget的重新build,并没有将build最小化。

我们今天就来解决如何避免不必要的build构建,将build缩小到最小的CountText。

分析

首先我们来分析下为什么会导致父widget的重新build。

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: CountInheritedWidget(
            count: count,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CountText(),
                RaisedButton(
                  onPressed: () => setState(() => count++),
                  child: Text("Increment"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}
为了方便分析,我把之前的代码提到这里来。

我们来看,在点击RaisedButton的时候,我们会通过setState将count进行更新。而此时的setState方法的提供者是_CountState,即CountWidget。而state的改变会导致build的重新构建,导致的效果是CountWidget的build被重新调用,继而它的子widget也相继被重新build。

既然已经知道了原因,那么我们再来思考下解决方案。

  1. 最简单的,我们缩小setState提供者的范围。现在是CountWidget,我们将其缩小到Column。
  2. 虽然已经缩小到了Column,但还是无法避免自身的build与其CountText之外的子Widget(RaisedButton)的重新build。如果我们将Column全部缓存下来呢?我们在Column外层套一个Widget,并将其进行缓存,一旦外层的Widget重新build,我们都使用Column的缓存,这样不就避免了Column的重新build。不过使用缓存以后会有个问题,既然是缓存,Center里面的CountText也将不会改变。为了解决这个问题,我们就要使用上篇文章中的InheritedWidget。将整个Column放到InheritedWidget中,虽然Column是缓存,但是CountText中引用了InheritedWidget中的count数据,一旦count发生改变,将会通知其进行重新build。这样就保证了只刷新CountText。
如果你对InheritedWidget不熟悉,推荐阅读从零开始的Flutter之旅: InheritedWidget

我们来总结一下,在Column外套一层Widget,并将Column进行缓存,然后外层的Widget结合InheritedWidget来提供共享count的数据源。一旦count更新将会调用外层Widget的setState,并且重新build,但我们使用的是Column缓存,同时CountText通过依赖的方式引用了共享的count数据源,从而会同步build更新。而RaisedButton使用的是未依赖的共享count数据源,所以并不会重新build。这样就保证了只刷新CountText。

这种方式统一定义为Provider,其实Flutter内部已经有Provider的完整实现,不过我们为了学习这种解决方法的思想,自己来实现一个简易版的Provider。之后再去看Flutter的Provider将会更加简单。

方案已经有了,下面我们直接来看具体实现细节。

实现

  1. 定义共享数据的ProviderInheritedWidget
  2. 定义监听刷新的NotifyModel
  3. 提供缓存Widget的ModelProviderWidget
  4. 组装替换原有实现方案

ProviderInheritedWidget

实现一个自己的InheritedWidget,主要用来提供共享数据源,并接受缓存的child。

class ProviderInheritedWidget<T> extends InheritedWidget {
  final T data;
  final Widget child;
 
  ProviderInheritedWidget({@required this.data, this.child})
      : super(child: child);
 
  @override
  bool updateShouldNotify(ProviderInheritedWidget oldWidget) {
    // true -> 通知树中依赖改共享数据的子widget
    return true;
  }
}

NotifyModel

为了监听共享数据count的变化,我们通过观察者订阅模式来实现。

class NotifyModel implements Listenable {
  List _listeners = [];
 
  @override
  void addListener(listener) {
    _listeners.add(listener);
  }
 
  @override
  void removeListener(listener) {
    _listeners.remove(listener);
  }
 
  void notifyDataSetChanged() {
    _listeners.forEach((item) => item());
  }
}

Listenable提供一个简单的监听接口,通过add与remove来增加与移除监听,然后提供一个notify方法来进行通知监听者。

最后我们通过继承NotifyModel来使count具有可监听能力

class CountModel extends NotifyModel {
  int count = 0;
 
  CountModel({this.count});
 
  void increment() {
    count++;
    notifyDataSetChanged();
  }
}

一旦count自增,就调用notifyDataSetChanged来通知订阅的监听者。

ModelProviderWidget

有了上面的Provider与Model,我们在提供一个外部Widget来统一管理它们,将它们结合起来。

class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget {
  final T data;
 
  final Widget child;
 
  // context 必须为当前widget的context
  static T of<T>(BuildContext context, {bool listen = true}) {
    return (listen ? context.dependOnInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
            : (context.getElementForInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
        .widget as ProviderInheritedWidget<T>)).data;
  }
 
  ModelProviderWidget({Key key, @required this.data, @required this.child})
      : super(key: key);
 
  @override
  _ModelProviderState<T> createState() => _ModelProviderState<T>();
}
 
class _ModelProviderState<T extends NotifyModel>
    extends State<ModelProviderWidget> {
  void notify() {
    setState(() {
      print("notify");
    });
  }
 
  @override
  void initState() {
    // 添加监听
    widget.data.addListener(notify);
    super.initState();
  }
 
  @override
  void dispose() {
    // 移除监听
    widget.data.removeListener(notify);
    super.dispose();
  }
 
  @override
  void didUpdateWidget(ModelProviderWidget<T> oldWidget) {
    // data 更新时移除老的data监听
    if (oldWidget.data != widget.data) {
      oldWidget.data.removeListener(notify);
      widget.data.addListener(notify);
    }
    super.didUpdateWidget(oldWidget);
  }
 
  @override
  Widget build(BuildContext context) {
    return ProviderInheritedWidget<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

在这里我们提供可监听的data数据与需要缓存的child,同时在state中对可监听的data在合适的地方进行监听订阅与移除订阅,并在收到data数据改变时调用notify进行setState操作,通知widget刷新。

在build中引用了ProviderInheritedWidget,来实现对共享子widget的数据共享,同时在ModelProviderWidget中提供of方法来暴露ProviderInheritedWidget的统一获取方式。

通过参数listen(默认true)来控制获取共享数据的方式,来决定是否建立依赖关系,即共享数据改变时,引用共享数据的widget是否重新build。

这一幕是不是有点似曾相识,基本上都是上篇文章中提到的InheritedWidget使用的细节。

接下来就是最终的方案替换

组装替换原有实现方案

我们通过ModelProviderWidget.of来获取共享的数据,所以只要使用到了共享数据,将要调用该方法。为了避免不必要的重复书写,我们将其单独封装到Consumer中,内部来实现对其的调用,并且将调用的结果暴露出来。

class Consumer<T> extends StatelessWidget {
  final Widget Function(BuildContext context, T value) builder;
 
  const Consumer({Key key, @required this.builder}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    print("Consumer build");
    return builder(context, ModelProviderWidget.of<T>(context));
  }
}

一切准备就绪,我们再对之前的代码进行优化。

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print("CountWidget build");
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: ModelProviderWidget<CountModel>(
            data: CountModel(count: 0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Consumer<CountModel>(
                    builder: (context, value) => Text("count: ${value.count}")),
                Builder(
                  builder: (context) {
                    print("RaiseButton build");
                    return RaisedButton(
                      onPressed: () => ModelProviderWidget.of<CountModel>(
                              context,
                              listen: false)
                          .increment(),
                      child: Text("Increment"),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

我们将Column缓存到ModelProviderWidget中,同时对CountModel数据进行共享;通过Consumer进行Text的封装,引用共享数据CountModel中的count。

对于RaisedButton,因为它只是提供点击,并且触发count的自增操作、没有发生ui上的任何变化。所以为了避免RaisedButton引用的共享数据进行自增时重新build,这里将listen参数置为false。

最后我们运行上面的代码,我们点击Increment按钮时,控制台将会输出如下日志:

inherited_widget_1.png

I/flutter ( 3141): notify
I/flutter ( 3141): Consumer build

说明只有Consumer重新调用了build,即Text进行了刷新。其它的widget都没有变化。

这样就解决了开篇提到的疑问,达到了widget刷新的最小化。

以上是一个简单地Provider-Consumer的使用。Flutter对这一块有更完善的实现方案。但是经过我们这一轮分析,你再去看Flutter中Provider的源码将会更加简单易懂。

如果你想了解Flutter中Provider的使用,你可以通过flutter_github来了解它的具体实战使用技巧。

想要查看Provider实战技巧,需要将分支切换到sample_provider

推荐项目

下面介绍一个完整的Flutter项目,对于新手来说是个不错的入门。

flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

flutter_github_preview.png

当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

Android补给站.jpg

查看原文

赞 6 收藏 4 评论 0

午后一小憩 发布了文章 · 6月9日

从零开始的Flutter之旅: InheritedWidget

inherited_widget_cover.png

往期回顾

从零开始的Flutter之旅: StatelessWidget

从零开始的Flutter之旅: StatefulWidget

在之前的文章中,介绍了StatelessWidget与StatefulWidget的特性与它们的呈现原理。

这期要聊的是它们的另一个兄弟InheritedWidget。

特性

InheritedWidget是Flutter中的一个非常重要的功能组件,它能够提供数据在widget树中从上到下进行传递。保证数据在不同子widget中进行共享。这对于一些需要使用共享数据的场景非常有效,例如,在Flutter SDK中就是通过InheritedWidget来共享应用的主题与语言信息。

可能你还有点模糊,别急,下面我们通过一个简单的示例来了解InheritedWidget。

示例

相信开始学Flutter时都看过官方的计数器示例。我们将官方提供的计数器示例使用InheritedWidget进行改造。

首先我们需要一个CountInheritedWidget,它继承于InheritedWidget。

class CountInheritedWidget extends InheritedWidget {
  CountInheritedWidget({@required this.count, Widget child})
      : super(child: child);
 
  // 共享数据,计数的数量
  final int count;
 
  // 统一的获取CountInheritedWidget实例, 方便树中子widget的获取共享数据
  // 必须在State中调用才会有效
  static CountInheritedWidget of(BuildContext context) {
    // 调用共享数据的子widget将不会回调didChangeDependencies方法,即子widget将不会更新
    // return context.getElementForInheritedWidgetOfExactType<CountInheritedWidget>().widget;
    return context.dependOnInheritedWidgetOfExactType<CountInheritedWidget>();
  }
 
  // true -> 通知树中依赖改共享数据的子widget
  @override
  bool updateShouldNotify(CountInheritedWidget oldWidget) {
    return oldWidget.count != count;
  }
}
  1. 在CountInheritedWidget中提供共享计数的数量count
  2. 同时为外部提供统一的获取CountInheritedWidget实例的of方法
  3. 最后再重写updateShouldNotify方法,来通知依赖该共享count的子widget进行更新

现在已经有了共享数据count的提供,接下来是在具体的子widget中进行使用。

我们抽离出一个CountText子widget

class CountText extends StatefulWidget {
  @override
  _CountTextState createState() {
    return _CountTextState();
  }
}
 
class _CountTextState extends State<CountText> {
  @override
  Widget build(BuildContext context) {
    return Text("count: ${CountInheritedWidget.of(context).count.toString()}");
  }
 
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}
  1. 内部引用了CountInheritedWidget中的共享数据count,通过of方法获取CountInheritedWidget实例
  2. didChangeDependencies可以用来监听子widget依赖是否反生改变

最后,我们再将CountInheritedWidget与CountText结合起来,通过简单的点击自增事件来看下效果

class CountWidget extends StatefulWidget {
  @override
  _CountState createState() {
    return _CountState();
  }
}
 
class _CountState extends State<CountWidget> {
  int count = 0;
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Count App',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("Count"),
        ),
        body: Center(
          child: CountInheritedWidget(
            count: count,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CountText(),
                RaisedButton(
                  onPressed: () => setState(() => count++),
                  child: Text("Increment"),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

上面的层级关系是CountText刚好是CountInheritedWidget的子widget。

现在我们通过点击事件直接改变外部的count值,如果InheritedWidget从上到下数据传到的效果能够生效,那么在CountText中引用的count将会与外部count同步,程序呈现的效果将会是自增的,同时由于依赖的count发生改变CountText中的didChangeDependencies也会回调。

我们直接运行一下

inherited_widget_1.png

点击后的输出日志

I/flutter: didChangeDependencies

说明InheritedWidget的效果已经生效,通过InheritedWidget的使用,我们可以很方便的在嵌套下层的子widget中拿到上层的数据,或者说整个widget的共享数据。

分析

在依赖方式改变时子widget的didChangeDependencies会回调,但由于你可能会在该方法中做一些特殊的操作,例如网络请求。只是需要一次就可以。如果是套用我们上面的示例,将会在count子增时反复调用。

为了防止didChangeDependencies的调用,我们再来看CountInheritedWidget的of方法中注释的那部分

  static CountInheritedWidget of(BuildContext context) {
    // 调用共享数据的子widget将不会回调didChangeDependencies方法,即子widget将不会更新
    // return context.getElementForInheritedWidgetOfExactType<CountInheritedWidget>().widget;
    return context.dependOnInheritedWidgetOfExactType<CountInheritedWidget>();
  }

我们使用的是dependOnInheritedWidgetOfExactType方法,依赖的共享数据发生改变时会回调子widget中的didChangeDependencies方法,如果我们不想要子widget调用该方法,可以使用注释的代码,通过getElementForInheritedWidgetOfExactType方法来获取共享数据。

如果此时我们再运行一下项目,点击count自增,控制台将不会再输出日志。这样就可以解决didChangeDependencies的反复调用。

而这两个方法的主要区别是在dependOnInheritedWidgetOfExactType调用的过程中会进行注册依赖关系

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

所以dependOnInheritedWidgetOfExactType更新依赖的子widget中的didChangeDependencies方法。

思考下一个问题,虽然现在didChangeDependencies方法不会调用,但是CountText的build方法还是会执行。原因是在CountWidget中通过setState来改变count值,会重新build所用的子widget。但我们真正想要的只是更新子widget中依赖的CountInheritedWidget的组件值。

那么如何解决呢?这里提供一个解决方案是为子widget提供缓存。可以通过封装一个简单的StatefulWidget,将子widget缓存起来。如果对这块感兴趣的,可以期待我的后续文章。

推荐项目

下面介绍一个完整的Flutter项目,对于新手来说是个不错的入门。

flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

flutter_github_preview.png

当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

Android补给站.jpg

查看原文

赞 6 收藏 6 评论 0

午后一小憩 发布了文章 · 3月16日

从零开始的Flutter之旅: StatefulWidget

往期回顾

从零开始的Flutter之旅: StatelessWidget

在之前的文章中,我们介绍了StatelessWidget的特性与它在Flutter中的呈现原理。

这次我们接着来聊聊它的兄弟StatefulWidget,俗称有状态小部件。

特性

如果你看了我之前的文章,你可能已经非常熟悉无状态小部件StatelessWidget。它们是由一个蓝图与不可变的element配置来实现的,实际安装到屏幕上的是各个StatelessElement。

不可变的东西我是非常喜欢的,就像写代码一样,一旦定义了一个不可变的变量,我就不用再关心它之后的所有事情,因为它不可变的性质,致使它不会发生不可预期的问题,只需直接使用它即可。

但一个程序只有不可变的配置是不行的,我们不可能编写一个只绘制一次后就停止的应用。因为一旦数据改变,不可变的配置是不可能帮助我们刷新ui,达到我们预期的效果;而有状态小部件StatefulWidget却可以轻松解决这些事情。

StatefulWidget提供不可变的配置信息以及可以随着时间变化而触发的状态对象;通过监听状态的变化来达到ui的更新。

简单点,我们从flutter_github挑选一个实例。

当我们点击其中一个未读通知信息时,我们需要将其ui状态变成已读的样式。根据状态来改变ui,StatefulWidget能够很好的实现这种场景。来看一下其实现

class NotificationTabPage extends BasePage<_NotificationPageState> {
  const NotificationTabPage();
 
  @override
  _NotificationPageState createBaseState() => _NotificationPageState();
}
 
class _NotificationPageState
    extends BaseState<NotificationVM, NotificationTabPage> {
  @override
  NotificationVM createVM() => NotificationVM(context);
 
  @override
  Widget createContentWidget() {
    return RefreshIndicator(
      onRefresh: vm.handlerRefresh,
      child: Scrollbar(
        child: ListView.builder(
          itemCount: vm.notifications?.length ?? 0,
          itemBuilder: (BuildContext context, int index) {
            final NotificationModel item = vm.notifications[index];
            return GestureDetector(
              onTap: () {
                vm.contentTap(index);
              },
              child: Container(
                color: item.unread ? Colors.white : Color.fromARGB(13, 0, 0, 0),
                padding: EdgeInsets.only(left: 15.0, top: 10.0, right: 15.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      item.repository.fullName,
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 16.0,
                        color: item.unread
                            ? Colors.black87
                            : Color.fromARGB(255, 102, 102, 102),
                      ),
                    ),
                    Row(
                      children: <Widget>[
                        Padding(
                          padding: EdgeInsets.only(top: 5.0),
                          child: Image.asset(
                            vm.getTypeFlagSrc(item.subject.type),
                            width: 18.0,
                            height: 18.0,
                          ),
                        ),
                        Expanded(
                          child: Padding(
                            padding: EdgeInsets.only(top: 5.0, left: 10.0),
                            child: Text(
                              item.subject.title,
                              overflow: TextOverflow.ellipsis,
                              maxLines: 1,
                              style: TextStyle(
                                fontSize: 14.0,
                                color: item.unread
                                    ? Color.fromARGB(255, 17, 17, 17)
                                    : Color.fromARGB(255, 102, 102, 102),
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                    Padding(
                      padding: EdgeInsets.only(top: 10.0),
                      child: Divider(
                        height: 1.0,
                        endIndent: 0.0,
                        color: Color.fromARGB(255, 207, 216, 220),
                      ),
                    ),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

这里的BasePage是MSVM架构中的基类,它继承于StatefulWidget;_NotificationPageState也是一样,它继承于State

abstract class BasePage<S extends BaseState>
    extends StatefulWidget {
    ...
}
 
abstract class BaseState<VM extends BaseVM, T extends StatefulWidget>
    extends State implements VMSContract {
    ...
}
关于MSVM后续会专门开文章介绍,想了解的可以期待一下

我们来看createContentWidget方法中的布局,找到上述情况关联的ui,在ListView的item中。

item布局的状态是根据item.unread来判断的,未读状态为ture。

当用户onTap点击时,将会向服务器发送thread阅读请求,当请求成功之后,再将相应位置的item.unread值改为false。

但就这样改变你会发现ui是不会刷新的,因为在StatefulWidget,如果你想改变某个值,同时要同步更新ui,需要使用setState方法。

  _markThreadRead(int index) async {
    try {
      Response response =
          await dio.patch('/notifications/threads/${_notifications[index].id}');
      if (response != null &&
          response.statusCode >= 200 &&
          response.statusCode < 300) {
        _notifications[index].unread = false;
        notifyStateChanged();
      }
    } on DioError catch (e) {
      Toast.show('makThreadRead error: ${e.message}', context);
    }
  }

这里将setState方法封装到notifyStateChanged方法中。所以现在再回过去看ui,会发现ui已经刷新了。

以上是使用StatefulWidget来达到ui的动态改变。再对比于之前的StatelessWidget,它们之间的区别显而易见了。

呈现原理

与StatelessWidget一样,接下来看下StatefulWidget的呈现原理。

StatefulWidget也是继承于Widget,所以它的内部也是存在createElement方法。本质也是通过createElement来创建对应的Element Tree,只不过创建的是StatefulElement;然后再调用对应的Widget Tree中的build方法来获取相应的蓝图。

但与StatelessWidget所不同的是,它还有另外一个方法

  @protected
  State createState();

通过createState来创建对应的State。StatefulWidget保留了StatelessWidget的特性,即保证final数据的不变性,而对于非final可变数据,将通过Stete进行管理。

上面是之前StatelessWidget呈现原理图,下面来对照看下StatefulWidget的。

除了Widget Tree与Element Tree,还有对应的State,它管理着可变的数据,例如item.unread。

一旦item.unread改变了,且通知到State,State将会再下一帧重新要求Widget Tree进行刷新。重新构建一个Container

由于是同一种类型Container,将会直接被替换,同时使用更新后的item.unread,所以对应的Container的color也将发生改变。最终呈现的是布局的刷新。

值得一提的是,State依附于Element Tree中,所以它的生命周期非常长,即使Widget Tree中的NotificationTabPage被移除重建,只要保证重建的类型是一致的,同时Widget Tree 与Element Tree的对应位置是没有变化的,那么Widget可以避免重建,只是会将其标记为脏状态,然后它的子widget将会通过build方法进行重建,替换State中的变化的值。

如果你要监听Widget的变化,可以重写didUpdateWidget

  @override
  void didUpdateWidget(StatefulWidget oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
  }

综上所述,StatefulWidget使你可以随时跟踪数据的变化并更新应用的ui。但你深入Flutter之后,你会发现自己写的更多的是StatelessWidget,因为需要用到的StatefulWidget基本上已经实现了,我们更多的是对StatelessWidget的封装,是不是很有意思呢,期待你的加入。

文中的代码都是来自于flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

下期预告

从零开始的Flutter之旅: InheritWidget

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,可以点击一下我的头像进行关注,当然您也可以关注我微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

查看原文

赞 6 收藏 5 评论 0

午后一小憩 发布了文章 · 3月7日

从零开始的Flutter之旅: StatelessWidget

Flutter - Beautiful native apps in record time.png

这次要展示的是什么是Flutter的Widget,即小部件;以及如何在Flutter中使用StatelessWidget,即无状态小部件。

至于Flutter,通俗的讲是开发者可以通一套简单的代码来同时构建Android与IOS应用程序。

特性

小部件是Flutter应用程序的基本构建模块,每一个都是不可变的声明,也是用户界面的一部分。例如button,text,color以及布局所用到的padding等等。

下面我们来看flutter_github中的一个实例。

stateless_widget_1.png

圈选中的item只有两个信息,头像与名称。为了避免代码的重复使用,将其抽离成一个独立的widget,具体代码如下

class FollowersItemView extends StatelessWidget {
  final GestureTapCallback tapCallback;
  final String avatarUrl;
  final String name;
 
  const FollowersItemView(
      {Key key, this.avatarUrl, this.name, this.tapCallback})
      : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 15.0),
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: tapCallback,
        child: Column(
          children: <Widget>[
            Row(
              children: <Widget>[
                FadeInImage.assetNetwork(
                  placeholder: 'images/app_welcome.png',
                  image: avatarUrl,
                  width: 80.0,
                  height: 80.0,
                ),
                Expanded(
                  child: Padding(
                    padding: EdgeInsets.only(left: 15.0),
                    child: Text(
                      name,
                      overflow: TextOverflow.ellipsis,
                      maxLines: 1,
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 20.0,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                )
              ],
            ),
            Padding(
              padding: EdgeInsets.symmetric(vertical: 15.0),
              child: Divider(
                thickness: 1.0,
                color: colorEAEAEA,
                height: 1.0,
                endIndent: 0.0,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

它继承于StatelessWidget,StatelessWidget的特性是无状态,数据不可变化。这个性质正好符合我们将要抽离的部件。抽离的部件需要做头像与名称的展示,没有任何形式上的交互变化。唯一的一个交互也是点击,但它并没有涉及数据的改变。所以在代码中将这些数据定义成final类型。本质就如Text部件,并没有如输入文本或者带有动画的部件一样随着时间内部属性会有所变化。

既然没有任何变化,那么我们也可以将其构造函数定义为const类型。

有了上面的部件抽离,我们就可以直接在ListView中使用该无状态部件

  @override
  Widget createContentWidget() {
    return RefreshIndicator(
      onRefresh: vm.handlerRefresh,
      child: Scrollbar(
        child: ListView.builder(
            padding: EdgeInsets.only(top: 15.0),
            itemCount: vm.list?.length ?? 0,
            itemBuilder: (BuildContext context, int index) {
              final item = vm.list[index];
              return FollowersItemView(
                avatarUrl: item.avatar_url,
                name: item.login,
                tapCallback: () {
                  Navigator.push(context, MaterialPageRoute(builder: (_) {
                    return WebViewPage(title: item.login, url: item.html_url);
                  }));
                },
              );
            }),
      ),
    );
  }

在ListView中引用FollowItemView,并传入不变的数据即可。

呈现原理

现在StatelessWidget的使用大家都会了,那它是如何调用的呢?

下面我们来看下它的呈现原理。

正如开头所说的将小部件作为Flutter应用构建的基础,在Flutter中我们将小部件的构建称作为Widget Tree,即小部件树。它就像是应用程序的蓝图,我们将蓝图创建好,然后内部会通过蓝图去创建对应显示在屏幕上的element元素。它包含了蓝图上对应的小部件的配置信息。所以对应的还有一个Element Tree,即元素树。

每一个StatelWidget都有一个StatelessElement,内部会通过createElement()方法进行创建其实例

  @override
  StatelessElement createElement() => StatelessElement(this);

同时在StatelessElement中会通过buid()方法来获取StalessWidget中所构建的蓝图Widget,并将元素显示到屏幕上。

Widget Tree与Element Tree之间的交互如下

stateless_widget_2.png

FollowerItemView中的StatelessElement会调用build方法来获取它是否有子部件,如果有的话对应的子部件也会创建它们自己的Element,并把它安装到元素树上。

所以我们的程序有两颗对应的树,其中一颗代表屏幕上显示的内容Element;另一颗树代表其展示的蓝图Widget,它们由许多的小部件组成。

而我们开发人员所做的就是将这些不同的小部件构建成我们所需要的应用程序。

最后,我们再来了解下最初的安装入口。

void main() {
  runApp(GithubApp());
}

在我们的main文件中,有一个main函数,其中调用了runApp方法,传入的是GithubApp。我们再来看下GithubApp是什么?

class GithubApp extends StatefulWidget {
  @override
  _GithubAppState createState() {
    return _GithubAppState();
  }
}
 
class _GithubAppState extends State<GithubApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Github',
      theme: ThemeData.light(),
      initialRoute: welcomeRoute.routeName,
      routes: {
        welcomeRoute.routeName: (BuildContext context) => WelcomePage(),
        loginRoute.routeName: (BuildContext context) => LoginPage(),
        homeRoute.routeName: (BuildContext context) => HomePage(),
        repositoryRoute.routeName: (BuildContext context) => RepositoryPage(),
        followersRoute.routeName: (BuildContext context) =>
            FollowersPage(followersRoute.pageType),
        followingRoute.routeName: (BuildContext context) =>
            FollowersPage(followingRoute.pageType),
        webViewRoute.routeName: (BuildContext context) => WebViewPage(title: '',),
      },
    );
  }
}

发现没它其实也是一个Widget,正如文章开头所说的,Flutter是由各个Widget组成。main是程序的入口,而其中的runApp中的Widget是整个程序挂载的起点。它会创建成一个具有与屏幕宽高一致的根元素,并把它装载到屏幕中。

所以在Flutter中一直都是通过创建Element,然后调用build方法来获取其后续的子Widget,最终构建成我们所看到的程序。

文中的代码都是来自于flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

flutter_github.png

当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

下期预告

从零开始的Flutter之旅: StatefulWidget

如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,建议您关注我的微信公众号:【Android补给站】

或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

Android补给站.jpg

查看原文

赞 8 收藏 7 评论 0

午后一小憩 发布了文章 · 1月31日

AwesomeGithub组件化探索之旅

awesome_github_page_header_image.jpeg

之前一直听说过组件化开发,而且面试也有这方面的提问,但都未曾有涉及具体的项目。所以就萌生了基于Github的开放Api,并使用组件化的方式来从零搭建一个Github客户端,起名为AwesomeGithub

在这里对组件化开发进行一个总结,同时也希望能够帮助别人更好的理解组件化开发。

先来看下项目的整体效果

awesome_github.png

下面是项目的结构

awesome_github_project.jpg

为何要使用组件化

  1. 对于传统的开发模式,一个app下面是包含项目的全部页面模块与逻辑。这样项目一旦迭代时间过长,业务模块逐渐增加,相应的业务逻辑复杂度也成指数增加。模块间的互相调用频繁,这样必定会导致模块间的耦合增加,业务逻辑嵌套程度加深。一旦修改其中一个模块,可能就牵一发动全身了。
  2. 传统的开发模式不利于团队的集体开发合作,因为每个开发者都是在同一个app模块下开发。这样导致的问题是,不能预期每个开发者所会修改到的具体代码部分,即所能够修改的代码区域。因为模块耦合在一起,涉及的区域不可预期,导致不同开发者会修改同一个文件或者同一段代码逻辑,从而导致异常冲突。
  3. 传统开发模式不利于测试,每次迭代都要将项目整体测试一遍。因为在同一个app下面代码是缺乏约束的,你不能保证只修改了迭代过程中所涉及的需求逻辑。

以上问题随着项目的迭代周期的增大,会表现的越来越明显。那么使用组件化又能够解决什么问题了?

组件化能够解决的问题

  1. 组件化开发是将各个相关功能进行分离,分别独立成一个单独可运行的app,并且组件之间不能相互直接引用。这样就减少了代码耦合,达到业务逻辑分层效果。
  2. 组件化可以提高团队协作能力,不同的人员可以开发不同的组件,保证不同开发人员互不影响。
  3. 组件化将app分成多个可单独运行的子项目,可以用自己独立的版本,可以独立编译,打包、测试与部署。这样不仅可以提高单个模块的编译速度,同时也可以提高测试的效率。
  4. 组件化可以提高项目的灵活性,app可以按需加载所要有的组件,提高app的灵活性,可以快速生成可定制化的产品。

现在我们已经了解了组件化的作用,但要实现组件化,达到其作用,必须解决实现组件化过程中所遇到的问题。

组件化需要解决的问题

  1. 组件单独运行
  2. 组件间数据传递
  3. 主项目使用组件中的Fragment
  4. 组件间界面的跳转
  5. 组件解耦

以上是实现组件化时所遇到的问题,下面我会结合AwesomeGithub来具体说明解决方案。

组件单独运行

组件的创建,可以直接使用library的方式进行创建。只不过在创建完之后,要让组件达到可以单独运行调试的地步,还需要进行相关配置。

运行方式动态配置

首先,当创建完library时,在build.gradle中可以找到这么一行代码

apply plugin: 'com.android.library'

这是gradle插件所支持的一种构建类型,代表可以将其依赖到主项目中,构建后输出aar包。这种方式对于我们将组件依赖到主项目中完全吻合的。

而gradle插件的另一种构建方式,可以在主项目的build.gradle中看到这么一行代码

apply plugin: 'com.android.application'

这代表在项目构建后会输出apk安装包,是一个独立可运行的项目。

明白了gradle的这两种构建方式之后,我们接下需要做的事也非常明了:需要将这两种方式进行动态配置,保证组件在主项目中以library方式存在,而自己单独的时候,则以application的方式存在。

下面我以AwesomeGithub中的login组件为例。

首先我们在根项目的gradle.properties中添加addLogin变量

addLogin = true

然后在login中的build.gradle通过addLogin变量来控制构建方式

if (addLogin.toBoolean()) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

这样就实现了对login的构建控制,可单独运行,也可依赖于app项目。

ApplicationId与AndroidManifest

除了修改gradle的构建方式,还需要动态配置ApplicationId与AndroidManifest文件。

有了上面的基础,实现方式也很简单。

可以在defaultConfig中增加对applicationId的动态配置

    defaultConfig {
        if (!addLogin.toBoolean()) {
            applicationId "com.idisfkj.awesome.login"
        }
        minSdkVersion Versions.min_sdk
        targetSdkVersion Versions.target_sdk
        versionCode Versions.version_code
        versionName Versions.version_name
 
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

而AndroidManifest文件可以通过sourceSets来配置

    sourceSets {
        main {
            if (addLogin.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            }
        }
    }

awesome_github_login.jpg

同时addLogin也可以作用于app,让login组件可配置依赖

awesome_github_login_main.jpg

这样login组件就可以独立于app进行单独构建、打包、调试与运行。

组件间的数据传递

由于组件与组件、项目间是不能直接使用类的相互引用来进行数据的传递,所以为了解决这个问题,这里通过一个公共库来做它们之间调用的桥梁,它们不直接拿到具体的引用对象,而是通过接口的方式来获取所需要的数据。

AwesomeGithub中我将其命名为componentbridge,各个组件都依赖于该公共桥梁,通过该公共桥梁各个组件间可以轻松的实现数据传递。

awesome_github_component_bridge.jpg

上图圈起来的部分都是componentbridge的重点,也是公共桥梁实现的基础。下面来分别详细说明。

BridgeInterface

这是公共桥梁的底层接口,每一个组件要向外实现自己的桥梁都要实现这个接口。

interface BridgeInterface {

    fun onClear() {}
}

内部很简单,只有一个方式onClear(), 用来进行数据的释放。

BridgeStore

用来做数据存储,对桥梁针对不同的key进行缓存。避免桥梁内部的实例多次创建。具体实现方式如下:

class BridgeStore {
 
    private val mMap = HashMap<String, BridgeInterface>()
 
    fun put(key: String, bridge: BridgeInterface) {
        mMap.put(key, bridge)?.onClear()
    }
 
    fun get(key: String): BridgeInterface? = mMap[key]
 
    fun clear() {
        for (item in mMap.values) {
            item.onClear()
        }
        mMap.clear()
    }
}

Factory

桥梁的实例构建工厂,默认提供通过反射的方式来实例化不同的类。Factory接口只提供一个create方法,实现方式由子类自行解决

interface Factory {
 
    fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T
}

AwesomeGithub中提供了通过反射方式来实例化不同类的具体实现NewInstanceFactory

class NewInstanceFactory : Factory {
 
    companion object {
        val instance: NewInstanceFactory by lazy { NewInstanceFactory() }
    }
 
    override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T = try {
        bridgeClazz.newInstance()
    } catch (e: InstantiationException) {
        throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
    } catch (e: IllegalAccessException) {
        throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
    }
 
}

Factory的作用是通过抽象的方式来获取所需要类的实例,至于该类如何实例化,将通过create方法自行实现。

Provider

Provider是提供桥梁的注册与获取各个组件暴露的接口实现。通过register来统一各个组件向外暴露的桥梁类,最后再通过getBridge来获取具体的桥梁类,然后调用所需的相关方法,最终达到组件间的数据传递。

来看下BridgeProviders的具体实现

class BridgeProviders {
 
    private val mProvidersMap = HashMap<Class<*>, BridgeProvider>()
    private val mBridgeMap = HashMap<Class<*>, Class<*>>()
    private val mDefaultBridgeProvider = BridgeProvider(NewInstanceFactory.instance)
 
    companion object {
        val instance: BridgeProviders by lazy { BridgeProviders() }
    }
 
    fun <T : BridgeInterface> register(
        clazz: Class<T>,
        factory: Factory? = null,
        replace: Boolean = false
    ) = apply {
        if (clazz.interfaces.isEmpty() || !clazz.interfaces[0].interfaces.contains(BridgeInterface::class.java)) {
            throw RuntimeException("$clazz must implement BridgeInterface")
        }
        // 1. get contract interface as key, and save implement class to map value.
        // 2. get contract interface as key, and save bridgeProvider of implement class instance
        // to map value.
        clazz.interfaces[0].let {
            if (mProvidersMap[it] == null || replace) {
                mBridgeMap[it] = clazz
                mProvidersMap[it] = if (factory == null) {
                    mDefaultBridgeProvider
                } else {
                    BridgeProvider(factory)
                }
            }
        }
    }
 
    fun <T : BridgeInterface> getBridge(clazz: Class<T>): T {
        mProvidersMap[clazz]?.let {
            @Suppress("UNCHECKED_CAST")
            return it.get(mBridgeMap[clazz] as Class<T>)
        }
        throw RuntimeException("$clazz subClass is not register")
    }

    fun clear() {
        mProvidersMap.clear()
        mBridgeMap.clear()
        mDefaultBridgeProvider.bridgeStore.clear()
    }
}

每次register之后都会保存一个BridgeProvider实例,如果没有实现自定义的Factory,将会使用默认是mDefaultBridgeProvider,它内部使用的就是默认的NewInstanceFactory

class BridgeProvider(private val factory: Factory) {
 
    val bridgeStore = BridgeStore()
 
    companion object {
        private const val DEFAULT_KEY = "com.idisfkj.awesome.componentbridge"
    }
 
    fun <T : BridgeInterface> get(key: String, bridgeClass: Class<T>): T {
        var componentBridge = bridgeStore.get(key)
        if (bridgeClass.isInstance(componentBridge)) {
            @Suppress("UNCHECKED_CAST")
            return componentBridge as T
        }
        componentBridge = factory.create(bridgeClass)
        bridgeStore.put(key, componentBridge)
        return componentBridge
    }
 
    fun <T : BridgeInterface> get(bridgeClass: Class<T>): T =
        get(DEFAULT_KEY + "@" + bridgeClass.canonicalName, bridgeClass)
}

注册完之后就可以在任意的组件中通过调用桥梁的getBridge来获取组件向外暴露的方法,从而达到数据的传递。

我们来看下具体的使用示例。

AwesomeGithub项目使用的是Github Open Api,用到的接口基本都要AuthorizationBasic或者是AccessToken,而为了让每一个组件在调用接口时都能够正常获取到AuthorizationBasic或者AccessToken,所以提供了一个AppBridge与AppBridgeInterface来向外暴露这些数据,实现如下:

interface AppBridgeInterface: BridgeInterface {
 
    /**
     * 获取用户的Authorization Basic
     */
    fun getAuthorizationBasic(): String?
 
    fun setAuthorizationBasic(authorization: String?)
 
    /**
     * 获取用户的AccessToken
     */
    fun getAccessToken(): String?
 
    fun setAccessToken(accessToken: String?)
}
class AppBridge : AppBridgeInterface {
 
    override fun getAuthorizationBasic(): String? = App.AUTHORIZATION_BASIC
 
    override fun setAuthorizationBasic(authorization: String?) {
        App.AUTHORIZATION_BASIC = authorization
    }
 
    override fun getAccessToken(): String? = App.ACCESS_TOKEN
 
    override fun setAccessToken(accessToken: String?) {
        App.ACCESS_TOKEN = accessToken
    }
 
}

有了上面的桥梁接口,接下来需要做的是先在App主项目中进行注册

    private fun registerBridge() {
        BridgeProviders.instance.register(AppBridge::class.java, object : Factory {
            override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return AppBridge() as T
            }
        })
            .register(HomeBridge::class.java)
            .register(UserBridge::class.java)
            .register(ReposBridge::class.java)
            .register(FollowersBridge::class.java)
            .register(FollowingBridge::class.java)
            .register(NotificationBridge::class.java)
            .register(SearchBridge::class.java)
            .register(WebViewBridge::class.java)
    }

在注册AppBridge时使用的是自定义的Factory,这里只是为了简单展示自定义的Factory的使用,其实没有特殊需求可以与后面的bridge一样直接调用regiser进行注册。

注册完了之后就可以直接在需要的地方进行调用。首先在登录组件中将获取到的AuthorizationBasic或者AccessToken进行保存,以便被之后的组件进行调用。

以AccessToken为例,在login组件中的核心调用代码如下:

    fun getAccessTokenFromCode(code: String) {
        showLoading.value = true
        repository.getAccessToken(code, object : RequestCallback<Response<ResponseBody>> {
            override fun onSuccess(result: ResponseSuccess<Response<ResponseBody>>) {
                try {
                    appBridge.setAccessToken(
                        result.data?.body()?.string()?.split("=")?.get(1)?.split("&")?.get(
                            0
                        )
                    )
                    getUser()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
 
            override fun onError(error: ResponseError) {
                showLoading.value = false
            }
        })
    }

如上所示,只需调用appBridge.setAccessToken将数据进行保存;而appBridge可以通过如下获取

appBridge = BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)

现在已经有了AccessToken数据,为了避免每次调用接口都手动加入AccessToken,可以使用okhttp的Interceptor,即在network组件中进行统一加入。

class GithubApiInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()

        val appBridge =
            BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)
        Timber.d("intercept url %s %s %s", request.url(), appBridge.getAuthorizationBasic(), appBridge.getAccessToken())

        val builder = request.newBuilder()
        val authorization =
            if (!TextUtils.isEmpty(appBridge.getAuthorizationBasic())) "Basic " + appBridge.getAuthorizationBasic()
            else "token " + appBridge.getAccessToken()
        builder.addHeader("Authorization", authorization)
        val response = chain.proceed(builder.build())
        Timber.d("intercept url %s, response %s ,code %d", request.url(), response.body().toString(), response.code())
        return response
    }
}

这样就完成了将AccessToken从login组件到network组件间的传递。

单个组件中调用

以上是主项目中集成了login组件,login组件会提供AuthorizationBasic或者AccessToken。那么对于单个组件(组件可以单独运行),为了让组件单独运行时也能调通相关的接口,在调用的时候加入正确的AuthorizationBasic或者AccessToken。需要提供默认的AppBridgeInterface实现类。我这里命名为DefaultAppBridge

class DefaultAppBridge : AppBridgeInterface {
 
    override fun getAuthorizationBasic(): String? = BuildConfig.AUTHORIZATION_BASIC
 
    override fun setAuthorizationBasic(authorization: String?) {
 
    }
 
    override fun getAccessToken(): String? = BuildConfig.ACCESS_TOKEN
 
    override fun setAccessToken(accessToken: String?) {
 
    }
}

里面具体的AuthorizationBasic与AccessToken值可以通过BuildConfig获取,而值的定义可以在local.properities中进行设置

AuthorizationBasic="xxxx"
AccessToken="xxx"

因为每个组件都会依赖与桥梁componentbridge,所以将值配置到componentbridge的build中,具体如下:

android {
    compileSdkVersion Versions.target_sdk
    buildToolsVersion Versions.build_tools
 
    defaultConfig {
        minSdkVersion Versions.min_sdk
        targetSdkVersion Versions.target_sdk
        versionCode Versions.version_code
        versionName Versions.version_name
        buildConfigField "String", "AUTHORIZATION_BASIC", getProperties("AuthorizationBasic") + ""
        buildConfigField "String", "ACCESS_TOKEN", getProperties("AccessToken") + ""
 
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
 
}

有了默认的组件桥梁实现,现在只需在对应的组件Application中进行注册即可。

例如项目中的followers组件,单独运行时使用DefaultAppBridge来达到接口的正常调用。

class FollowersApp : Application() {
 
    override fun onCreate() {
        super.onCreate()
        SPUtils.init(this)
        initTimber()
        initRouter()
        // register bridges
        BridgeProviders.instance.register(DefaultAppBridge::class.java)
            .register(DefaultWebViewBridge::class.java)
    }
 
    private fun initTimber() {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        }
    }
 
    private fun initRouter() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)
    }
}

在组件单独运行时的Application中注册之后,单独运行时调用的就是local.properities中设置的值。即保证了组件正常单独运行。

以上是组件间数据传递的全部内容,即解决了组件间的数据传递也解决了组件单独运行时的默认数据调用问题。如需了解全部代码可以查看AwesomeGithub项目。

主项目使用组件中的Fragment

awesome_github_search.jpeg

AwesomeGithub主页有三个tab,分别是三个组件。这个三个组件是主页viewpager中的三个fragment。前面已经说了,在主项目中不能直接调用各个组件,那么组件中的fragment又该如何加入到主项目中呢?

其实也很简单,可以将获取fragment的实例当作为组件间的数据传递的一种特殊形式。那么有了上面的组件间数据传递的基础,实现在主项目中调用组件的fragment也瞬间简单了许多。借助的还是桥梁componentbridge。

下面以主页的search为例

SearchBridgeInterface

首先在componentbridge中创建SearchBridgeInterface接口,并且实现默认的桥梁的BridgeInterface接口。

interface SearchBridgeInterface : BridgeInterface {
 
    fun getSearchFragment(): Fragment
}

其中就一个方法,用来向外提供SearchFragment的获取

接下来在search组件中实现SearchBridgeInterface的具体实现类

class SearchBridge : SearchBridgeInterface {
 
    override fun getSearchFragment(): Fragment = SearchFragment.getInstance()
 
}

然后回到主项目的Application中进行注册

BridgeProviders.instance.register(SearchBridge::class.java)

注册完之后,就可以在主项目的ViewPagerAdapter中进行获取SearchFragment实例

class MainViewPagerAdapter(fm: FragmentManager?) : FragmentPagerAdapter(fm) {
 
    override fun getItem(position: Int): Fragment = when (position) {
        0 -> BridgeProviders.instance.getBridge(SearchBridgeInterface::class.java).getSearchFragment()
        1 -> BridgeProviders.instance.getBridge(NotificationBridgeInterface::class.java)
            .getNotificationFragment()
        else -> BridgeProviders.instance.getBridge(UserBridgeInterface::class.java).getUserFragment()
    }
 
    override fun getCount(): Int = 3
}

主项目中调用组件中的Fragment就是这么简单,基本上与之前的数据传递时一致的。

组件间界面的跳转

有了上面的基础,可能会联想到使用处理Fragment方式来进行组件间页面的跳转。的确这也是一种解决方式,不过接下来要介绍的是另一种更加方便与高效的跳转方式。

项目中使用的是ARouter,它是一个帮助App进行组件化改造的框架,支持模块间的路由、通信与解藕。下面简单的介绍下它的使用方式。

首先需要去官网找到版本依赖,并进行导入。这里不多说,然后需要在你所有用到的模块中的build.gradle中添加以下配置

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

记住只要该模块需要调用ARouter,就需要添加以上代码。配置完之后就可以开始使用。

下面我以项目中的webview组件为例,跳转到组件中的WebViewActivity

上面已经将相关依赖配置好了,首先需要在Application中进行ARouter初始化

    private fun initRouter() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)
    }

再为WebViewActivity进行path定义

object ARouterPaths {
    const val PATH_WEBVIEW_WEBVIEW = "/webview/webview"
}

因为每一个ARouter进行路由的时候,都需要配置一个包含两级的路径,然后将定义的路径配置到WebViewActivity中

@Route(path = ARouterPaths.PATH_WEBVIEW_WEBVIEW)
class WebViewActivity : BaseActivity<WebviewActivityWebviewBinding, WebViewVM>() {
 
    @Autowired
    lateinit var url: String
    @Autowired
    lateinit var requestUrl: String
 
    override fun getVariableId(): Int = BR.vm
 
    override fun getLayoutId(): Int = R.layout.webview_activity_webview
 
    override fun getViewModelInstance(): WebViewVM = WebViewVM()
 
    override fun getViewModelClass(): Class<WebViewVM> = WebViewVM::class.java
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ARouter.getInstance().inject(this)
        viewModel.url.value = url
        viewModel.request(requestUrl)
    }
 
    override fun addObserver() {
        super.addObserver()
        viewModel.backClick.observe(this, Observer {
            finish()
        })
    }
 
    override fun onBackPressed() {
        if (viewDataBinding.webView.canGoBack()) {
            viewDataBinding.webView.goBack()
            return
        }
        super.onBackPressed()
    }
 
}

如上所示,在进行配置时,只需在类上添加@Route注解,然后再将定义的路径配置到path上。其中的@Autowired注解代表WebViewActivity在使用ARouter进行跳转时,接收两个参数,分别为url与requestUrl。

ARouter本质是解析注解,然后定位到参数,再通过原始的Intent中获取到传递过来的参数值。

有了上面的准备过程,最后剩下的就是调用ARouter进行页面跳转。这里为了统一调用方式,将其调加到桥梁中。

class WebViewBridge : WebViewBridgeInterface {
 
    override fun toWebViewActivity(context: Context, url: String, requestUrl: String) {
        ARouter.getInstance().build(ARouterPaths.PATH_WEBVIEW_WEBVIEW).with(
            bundleOf("url" to url, "requestUrl" to requestUrl)
        ).navigation(context)
    }
 
}

前面是定义的跳转路径,后面紧接的是页面传递的参数值。剩下的就是在别的组件中调用该桥梁,例如followers组件中的contentClick点击:

class FollowersVHVM(private val context: Context) : BaseRecyclerVM<FollowersModel>() {
 
    var data: FollowersModel? = null
 
    override fun onBind(model: FollowersModel?) {
        data = model
    }
 
    fun contentClick() {
        BridgeProviders.instance.getBridge(WebViewBridgeInterface::class.java)
            .toWebViewActivity(context, data?.html_url ?: "", "")
    }
}
更多ARouter的使用方式,读者可以自行查阅官方文档

AwesomeGithub项目中,组件化过程中的主要难点与解决方案已经分析的差不多了。最后我们来聊聊组件间的解藕优化。

组件解耦

组件化本身就是对项目进行解藕,所以如果要进一步进行优化,主要是对组件间的依赖或者资源等方面进行解藕。而对于组件间的依赖,尝试过在依赖的时候使用runtimeOnly。因为runtimeOnly可以避免依赖的组件在运行之前进行引用调用,它只会在项目运行时才能够正常的引用,这样就可以防止主项目中进行开发时直接引用依赖的组件。

但是,在实践的过程中,如果项目中使用了DataBinding,此时使用runtimeOnly进行依赖组件,通过该方式依赖的组件在运行的过程中会出现错误。

awesome_github_error.png

这是由于DataBinding需要在编译时生成对应资源文件。使用runtimeOnly会导致其缺失,最终在程序进行运行时找不到对应资源,导致程序异常。

当然如果没有使用DataBinding就不会有这种问题。这是组件依赖方面,下面再来说说资源相关的。

由于不同组件模块下可以引入相同命名的资源文件,为了防止开发过程中不同组件下相同名称的资源文件引用错乱,这里可以通过在不同组件模块中的build.gradle中添加资源前缀。例如login组件中

awesome_github_resource.png

resourcePrefix代表login组件中的所有资源文件命名都必须以login_为前缀命名。如果没有编译器将会标红,并提示你正确的使用方式。这种方式可以一定程度上避免资源文件的乱用与错乱。

以上是AwesomeGithub组件化过程中的整个探索经历。如果你想更深入的了解其实现过程,强烈建议你直接查看项目的源码,毕竟语言上的描述是有限的,程序员就应该直接看代码才能更快更准的理解。

项目地址

AwesomeGithub: https://github.com/idisfkj/Aw...

如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!

Android补给站.jpg

查看原文

赞 8 收藏 6 评论 1

午后一小憩 发布了文章 · 2019-10-28

Android Navigation的四大要点你都知道吗?

在JetPack中有一个组件是Navigation,顾名思义它是一个页面导航组件,相对于其他的第三方导航,不同的是它是专门为Fragment的页面管理所设计的。它对于单个Activity的App来说非常有用,因为以一个Activity为架构的App页面的呈现都是通过不同的Fragment来展示的。所以对于Fragment的管理至关重要。通常的实现都要自己维护Fragment之间的栈关系,同时要对Fragment的Transaction操作非常熟悉。为了降低使用与维护成本,所以就有了今天的主角Navigation。

如果你对JetPack的其它组件感兴趣,推荐你阅读我之前的系列文章,本篇文章目前为JetPack系列的最后一篇。

Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
Paging在RecyclerView中的应用,有这一篇就够了
WorkManager从入门到实践,有这一篇就够了

对于Navigation的使用,我将其归纳于以下四点:

  • Navigation的基本配置
  • Navigation的跳转与数据传递
  • Navigation的页面动画
  • Navigation的deepLink

配置

在使用之前需要引入Navigation的依赖,然后我们需要为Navigation创建一个配置文件,它将位于res/navigation/nav_graph.xml。为了方便理解文章中的代码,我写了一个Demo,大家可以通过Android精华录查看。

在我的Demo中打开nav_graph.xml你将清晰的看到它们页面间的关系纽带

1.png

一共有6个页面,最左边的为程序入口页面,它们间的线条指向为它们间可跳转的方向。

我们再来看它们的xm配置👇

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/welcome_fragment">
 
    <fragment
        android:id="@+id/welcome_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
        android:label="welcome_fragment"
        tools:layout="@layout/fragment_welcome">
 
        <action
            android:id="@+id/action_go_to_register_page"
            app:destination="@id/register_fragment" />
 
        <action
            android:id="@+id/action_go_to_order_list_page"
            app:destination="@id/order_list_fragment"/>
 
    </fragment>
 
    <fragment
        android:id="@+id/register_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
        android:label="register_fragment"
        tools:layout="@layout/fragment_register">
 
        <action
            android:id="@+id/action_go_to_shop_list_page"
            app:destination="@id/shop_list_fragment" />
 
    </fragment>
     
    ...
</navigation>

页面标签主要包含navigation、fragment与action

  • navigation: 定义导航栈,可以进行嵌套定义,各个navigation相互独立。它有一个属性startDestination用来定义导航栈的根入口fragment
  • fragment: 顾名思义fragment页面。通过name属性来定义关联的fragment
  • action: 意图,可以理解为Intent,即跳转的行为。通过destination来关联将要跳转的目标fragment。

以上是nav_graph.xml的基本配置。

在配置完之后,我们还需要将其关联到Activity中。因为所有的Fragment都离不开Activity。

Navigation为我们提供了两个配置参数: defaultNavHost与navGraph,所以在Activity的xml中需要如下配置👇

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_light"
    android:orientation="vertical"
    tools:context=".navigation.NavigationMainActivity">
 
    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
 
</LinearLayout>
  • defaultNavHost: 将设备的回退操作进行拦截,并将其交给Navigation进行管理。
  • navGraph: Navigation的配置文件,即上面我们配置的nav_graph.xml文件

除此之外,fragment的name属性必须为NavHostFragment,因为它会作为我们配置的所有fragment的管理者。具体通过内部的NavController中的NavigationProvider来获取Navigator抽象实例,具体实现类是FragmentNavigator,所以最终通过它的navigate方法进行创建我们配置的Fragment,并且添加到NavHostFragment的FrameLayout根布局中。

此时如果我们直接运行程序后发现已经可以看到入口页面WelcomeFragment

2.png

但点击register等操作你会发现点击跳转无效,所以接下来我们需要为其添加跳转

跳转

由于我们之前已经在nav_graph.xml中定义了action,所以跳转的接入非常方便,每一个action的关联跳转只需一行代码👇

class WelcomeFragment : Fragment() {
 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_welcome, container, false).apply {
            register_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_register_page))
            stroll_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_order_list_page))
        }
    }
}

代码中的id就是配置的action的id,内部原理是先获取到对应的NavController,通过点击的view来遍历找到最外层的parent view,因为最外层的parent view会在配置文件导入时,即NavHostFragment中的onViewCreated方法中进行关联对应的NavController👇

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!(view instanceof ViewGroup)) {
            throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
        }
        Navigation.setViewNavController(view, mNavController);
        // When added programmatically, we need to set the NavController on the parent - i.e.,
        // the View that has the ID matching this NavHostFragment.
        if (view.getParent() != null) {
            View rootView = (View) view.getParent();
            if (rootView.getId() == getId()) {
                Navigation.setViewNavController(rootView, mNavController);
            }
        }
    }

然后再调用navigate进行页面跳转处理,最终通过FragmentTransaction的replace进行Fragment替换👇

    -------------- NavController ------------------
     
    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        # ---- 关键代码 -------
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        ....
    }
     
    -------------- FragmentNavigator ------------------
 
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
 
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
 
        # ------ 关键代码 ------
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);
        ...
    }

源码就分析到这里了,如果需要深入了解,建议阅读NavHostFragmentNavControllerNavigatorProviderFragmentNavigator

传参

以上是页面的无参跳转,那么对于有参跳转又该如何呢?

大家想到的应该都是bundle,将传递的数据填入到bundle中。没错Navigator提供的navigate方法可以进行传递bundle数据👇

findNavController().navigate(R.id.action_go_to_shop_detail_page, bundleOf("title" to "I am title"))

这种传统的方法在传递数据类型上并不能保证其一致性,为了减少人为精力上的错误,Navigation提供了一个Gradle插件,专门用来保证数据的类型安全。

使用它的话需要引入该插件,方式如下👇

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.1.0"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

最后再到app下的build.gradle中引入该插件👇

apply plugin: "androidx.navigation.safeargs.kotlin"

而它的使用方式也很简单,首先参数需要在nav_graph.xml中进行配置。👇

    <fragment
        android:id="@+id/shop_list_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopListFragment"
        android:label="shop_list_fragment"
        tools:layout="@layout/fragment_shop_list">
 
        <action
            android:id="@+id/action_go_to_shop_detail_page"
            app:destination="@id/shop_detail_fragment">
 
            <argument
                android:name="title"
                app:argType="string" />
 
        </action>
 
    </fragment>
 
    <fragment
        android:id="@+id/shop_detail_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopDetailFragment"
        android:label="shop_detail_fragment"
        tools:layout="@layout/fragment_shop_detail">
 
        <action
            android:id="@+id/action_go_to_cart_page"
            app:destination="@id/cart_fragment"
            app:popUpTo="@id/cart_fragment"
            app:popUpToInclusive="true" />
 
        <argument
            android:name="title"
            app:argType="string" />
 
    </fragment>

现在我们从ShopListFragment跳转到ShopDetailFragment,需要在ShopListFragment的对应action中添加argument,声明对应的参数类型与参数名,也可以通过defaultValue定义参数的默认值与nullable标明是否可空。对应的ShopDetailFragment接收参数也是一样。

另外popUpTo与popUpToInclusive属性是为了实现跳转到CartFragment时达到SingleTop效果。

下面我们直接看在代码中如何使用这些配置的参数,首先是在ShopListFragment中👇

holder.item.setOnClickListener(Navigation.createNavigateOnClickListener(ShopListFragmentDirections.actionGoToShopDetailPage(shopList[position])))

还是创建一个createNavigateOnClickListener,只不过现在传递的不再是跳转的action id,而是通过插件自动生成的ShopListFragmentDirections.actionGoToShopDetailPage方法。一旦我们如上配置了argument,插件就会自动生成一个以[类名]+Directions的类,而自动生成的类本质是做了跳转与参数的封装,源码如下👇

class ShopListFragmentDirections private constructor() {
    private data class ActionGoToShopDetailPage(val title: String) : NavDirections {
        override fun getActionId(): Int = R.id.action_go_to_shop_detail_page
 
        override fun getArguments(): Bundle {
            val result = Bundle()
            result.putString("title", this.title)
            return result
        }
    }
 
    companion object {
        fun actionGoToShopDetailPage(title: String): NavDirections = ActionGoToShopDetailPage(title)
    }
}

本质是将action id与argument封装成一个NavDirections,内部通过解析它来获取action id与argument,从而执行跳转。

而对于接受方ShopDetailFragment,插件页面自动帮我们生成一个ShopDetailFragmentArgs,以[类名]+Args的类。所以我们需要做的也非常简单👇

class ShopDetailFragment : Fragment() {
 
    private val args by navArgs<ShopDetailFragmentArgs>()
 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_shop_detail, container, false).apply {
            title.text = args.title
            add_cart.setOnClickListener(Navigation.createNavigateOnClickListener(ShopDetailFragmentDirections.actionGoToCartPage()))
        }
    }

}

通过navArgs来获取ShopDetailFragmentArgs对象,它其中包含了传递过来的页面数据。

动画

在action中不仅可以配置跳转的destination,还可以定义对应页面的转场动画,使用非常简单👇

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/welcome_fragment">
 
    <fragment
        android:id="@+id/welcome_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
        android:label="welcome_fragment"
        tools:layout="@layout/fragment_welcome">
 
        <action
            android:id="@+id/action_go_to_register_page"
            app:destination="@id/register_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
        <action
            android:id="@+id/action_go_to_order_list_page"
            app:destination="@id/order_list_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
    </fragment>
    ...
</navigation>

对应四个动画配置参数

  • enterAnim: 配置进场时目标页面动画
  • exitAnim: 配置进场时原页面动画
  • popEnterAnim: 配置回退pop时目标页面动画
  • popExitAnim: 配置回退pop时原页面动画

通过上面的配置你可以看到如下效果👇

3.gif

deepLink

我们回想一下对于多个Activity我需要实现deepLink效果,应该都是在AndroidManifest.xml中进行配置scheme、host等。而对于单个Activity也需要实现类似的效果,Navigation也提供了对应的实现,而且操作更简单。

Navigation提供的是deepLink标签,可以直接在nav_graph.xml进行配置,例如👇

    <fragment
        android:id="@+id/register_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
        android:label="register_fragment"
        tools:layout="@layout/fragment_register">
 
        <action
            android:id="@+id/action_go_to_shop_list_page"
            app:destination="@id/shop_list_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
        <deepLink app:uri="api://register/" />
 
    </fragment>

上面通过deepLink我配置了一个跳转到注册页RegisterFragment,写法非常简单,直接配置uri即可;同时还可以通过占位符配置传递参数,例如👇

<deepLink app:uri="api://register/{id}" />

这时我们就可以在注册页面通过argument获取key为id的数据。

当然要实现上面的效果,我们还需要一个前提,需要在AndroidManifest.xml中将我们的deepLink进行配置,在Activity中使用nav-graph标签👇

    <application
        ...
        android:theme="@style/AppTheme">
        <activity android:name=".navigation.NavigationMainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <nav-graph android:value="@navigation/nav_graph"/>
        </activity>
        ...
    </application>

现在只需将文章中的demo安装到手机上,再点击下面的link

jump to register api

之后就会启动App,并定位到注册界面。是不是非常简单呢?

最后我们再来看下效果👇

4.gif

有关Navigation暂时就到这里,通过这篇文章,希望你能够熟悉运用Navigation,并且发现单Activity的魅力。

如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!

项目地址

Android精华录

该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点

Android精华录

Android补给站.jpg

查看原文

赞 5 收藏 3 评论 0

午后一小憩 发布了文章 · 2019-08-15

WorkManager从入门到实践,有这一篇就够了

前言

上一次我们对Paging的应用进行了一次全面的分析,这一次我们来聊聊WorkManager。

如果你对Paging还未了解,推荐阅读这篇文章:

Paging在RecyclerView中的应用,有这一篇就够了

本来这一篇文章上周就能够发布出来,但我写文章有一个特点,都会结合具体的Demo来进行阐述,而WorkManager的Demo早就完成了,只是要结合文章一起阐述实在需要时间,上周自身原因也就延期了,想想还是写代码容易啊...😿😿

哎呀不多说了,进入正题!

WorkManager

WorkManager是什么?官方给的解释是:它对可延期任务操作非常简单,同时稳定性非常强,对于异步任务,即使App退出运行或者设备重启,它都能够很好的保证任务的顺利执行。

所以关键点是简单与稳定性。

对于平常的使用,如果一个后台任务在执行的过程中,app突然退出或者手机断网,这时后台任务将直接终止。

典型的场景是:App的关注功能。如果用户在弱网的情况下点击关注按钮,此时用户由于某种原因马上退出了App,但关注的请求并没有成功发送给服务端,那么下次用户再进入时,拿到的还是之前未关注的状态信息。这就产生了操作上的bug,降低了用户的体验,增加了用户不必要的操作。

那么该如何解决呢?很简单,看WorkManager的定义,使用WorkManager就可以轻松解决。这里就不再拓展实现代码了,只要你继续看完这篇文章,你就能轻松实现。

当然你不使用WorkManager也能实现,这就涉及到它的另一个好处:简单。如果你不使用WorkManager,你就要对不同API版本进行区分。

JobScheduler

val service = ComponentName(this, MyJobService::class.java)
val mJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val builder = JobInfo.Builder(jobId, serviceComponent)
 .setRequiredNetworkType(jobInfoNetworkType)
 .setRequiresCharging(false)
 .setRequiresDeviceIdle(false)
 .setExtras(extras).build()
mJobScheduler.schedule(jobInfo)

通过JobScheduler来创建一个Job,一旦所设的条件达到,就会执行该Job。但JobScheduler是在API21加入的,同时在API21&22有一个系统Bug

这就意味着它只能用在API23及以上的版本

if (Build.VERSION.SDK_INT >= 23) {
    // use JobScheduler
}

既然只能API23及以上才能使用JobScheduler,那么在API23以下又该如何呢?

AlarmManager & BroadcastReceiver

这时对于API23以下,可以使用AlarmManager来进行任务的执行,同时结合BoradcastReceiver来进行任务的条件监听,例如网络的连接状态、设备的启动等。

看到这里是不是开始头大了呢,我们开始的目的只是想做一个稳定性的后台任务,最后发现居然还要进行版本兼容。兼容性与实现性进一步加大。

那么有没有统一的实现方式呢?当然有,它就是WorkManager,它的核心原理使用的就是上面所分析的结合体。

他会结合版本自动使用最佳的实现方式,同时还会提供额外的便利操作,例如状态监听、链式请求等等。

WorkManager的使用,我将其分为以下几步:

  1. 构建Work
  2. 配置WorkRequest
  3. 添加到WorkContinuation中
  4. 获取响应结果

下面我们来通过Demo逐步了解。

构建Work

WorkManager每一个任务都是由Work构成,所以Work是任务具体执行的核心所在。既然是核心所在,你可能会认为它会非常难实现,但恰恰相反,它的实现非常简单,你只需实现它的doWork方法即可。例如我们来实现一个清除相关目录下的.png图片的Work

class CleanUpWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    override fun doWork(): Result {
        val outputDir = File(applicationContext.filesDir, Constants.OUTPUT_PATH)
        if (outputDir.exists()) {
            val fileLists = outputDir.listFiles()
            for (file in fileLists) {
                val fileName = file.name
                if (!TextUtils.isEmpty(fileName) && fileName.endsWith(".png")) {
                    file.delete()
                }
            }
        }
        return Result.success()
    }
}

所有代码都在doWork中,实现逻辑也非常简单:找到相关目录,然后逐一判断目录中的文件是否为.png图片,如果是就删除。

以上是逻辑代码,关键点是返回值Result.success(),它是一个Result类型,可用值有三个

  1. Result.success(): 成功
  2. Result.failure(): 失败
  3. Result.retry(): 重试

对于success与failure,它还支持传递Data类型的值,Data内部是一个Map来管理的,所以对于kotlin可以直接使用workDataOf

return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))

它传递的值将放入OutputData中,可以在链式请求中传递,与最终的响应结果获取。其实本质是WorkManager结合了Room,将数据保存在数据库中。

这一步要点就是这么多,下面进入下一步。

配置WorkRequest

WorkManager主要是通过WorkRequest来配置任务的,而它的WorkRequest种类包括:

  1. OneTimeWorkRequest
  2. PeriodicWorkRequest

OneTimeWorkRequest

首先OneTimeWorkRequest是作用于一次性任务,即任务只执行一次,一旦执行完就自动结束。它的构建也非常简单:

val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()

这样就配置了与CleanUpWorker相关的WorkRequest,而且是一次性的。

在配置WorkRequest的过程中我们还可以对其添加别的配置,例如添加tag、传入inputData与添加constraint约束条件等等。

val constraint = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
 
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
        .setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
        .addTag(Constants.TAG_BLUR_IMAGE)
        .setConstraints(constraint)
        .build()

添加tag是为了打上标签,以便后续获取结果;传入的inputData可以在BlurImageWork中获取传入的值;添加网络连接constraint约束条件,代表只有在网络连接的状态下才会触发该WorkRequest。

而BlurImageWork的核心代码如下:

override suspend fun doWork(): Result {
    val resId = inputData.getInt(Constants.KEY_IMAGE_RES_ID, -1)
    if (resId != -1) {
        val bitmap = BitmapFactory.decodeResource(applicationContext.resources, resId)
        val outputBitmap = apply(bitmap)
        val outputFileUri = writeToFile(outputBitmap)
        return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
    }
    return Result.failure()
}

在doWork中,通过InputData来获取上述blurRequest中传入的InputData数据。然后通过apply来处理图片,最后使用writeToFile写入到本地文件中,并返回路径。

由于篇幅有限,这里就不一一展开,感兴趣的可以查看源码

PeriodicWorkRequest

PeriodicWorkRequest是可以周期性的执行任务,它的使用方式与配置和OneTimeWorkRequest一致。

val constraint = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
 
// at least 15 minutes
mPeriodicRequest = PeriodicWorkRequestBuilder<DataSourceWorker>(15, TimeUnit.MINUTES)
        .setConstraints(constraint)
        .addTag(Constants.TAG_DATA_SOURCE)
        .build()

不过需要注意的是:它的周期间隔最少为15分钟。

添加到WorkContinuation中

上面我们已经将WorkRequest配置好了,剩下要做的是将其加入到work工作链中进行执行。

对于单个的WorkRequest,可以直接通过WorkManager的enqueue方法

private val mWorkManager: WorkManager = WorkManager.getInstance(application)
 
mWorkManager.enqueue(cleanUpRequest)

如果你想使用链式工作,只需调用beginWith或者beginUniqueWork方法即可。其实它们本质都是实例化了一个WorkContinuationImpl,只是调用了不同的构造方法。而最终的构造方法为:

    WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
            String name,
            ExistingWorkPolicy existingWorkPolicy,
            @NonNull List<? extends WorkRequest> work,
            @Nullable List<WorkContinuationImpl> parents) { }

其中beginWith方法只需传入WorkRequest

val workContinuation = mWorkManager.beginWith(cleanUpWork)

beginUniqueWork允许我们创建一个独一无二的链式请求。使用也很简单:

val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpWork)

其中第一个参数是设置该链式请求的name;第二个参数ExistingWorkPolicy是设置name相同时的表现,它三个值,分别为:

  1. REPLACE: 当有相同name且未完成的链式请求时,将原来的进度取消并删除,重新加入新的链式请求
  2. KEEP: 当有相同name且未完成的链式请求时,链式请求保持不变
  3. APPEND: 当有相同name且未完成的链式请求时,将新的链式请求追加到原来的子队列中,即当原来的链式请求全部执行后才开始执行。

而不管是beginWith还是beginUniqueWork,它都会返回WorkContinuation对象,通过该对象我们可以将后续任务加入到链式请求中。例如将上面的cleanUpRequest(清除)、blurRequest(图片模糊处理)与saveRequest(保存)串行起来执行,实现如下:

val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpRequest)
 
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
        .setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
        .addTag(Constants.TAG_BLUR_IMAGE)
        .build()
 
val saveRequest = OneTimeWorkRequestBuilder<SaveImageToMediaWorker>()
        .addTag(Constants.TAG_SAVE_IMAGE)
        .build()
 
workContinuation.then(blurRequest)
        .then(saveRequest)
        .enqueue()

除了串行执行,还支持并行。例如将cleanUpRequest与blurRequest并行处理,完成之后再与saveRequest串行

val left = mWorkManager.beginWith(cleanUpRequest)
val right = mWorkManager.beginWith(blurRequest)
 
WorkContinuation.combine(arrayListOf(left, right))
        .then(saveRequest)
        .enqueue()

需要注意的是:如果你的WorkRequest是PeriodicWorkRequest类型,那么它不支持建立链式请求,这一点需要注意了。简单的理解,周期性的任务原则上是没有终止的,是个闭环,也就不存在所谓的链了。

获取响应结果

这就到最后一步了,获取响应结果WorkInfo。WorkManager支持两种方式来获取响应结果

  1. Request.id: WorkRequest的id
  2. Tag.name: WorkRequest中设置的tag

同时返回的WorkInfo还支持LiveData数据格式。

例如,现在我们要监听上述blurRequest与saveRequest的状态,使用tag来获取:

// ViewModel
internal val blurWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_BLUR_IMAGE)
 
internal val saveWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_SAVE_IMAGE)
 
// Activity
private fun addObserver() {
    vm.blurWorkInfo.observe(this, Observer {
        if (it == null || it.isEmpty()) return@Observer
        with(it[0]) {
            if (!state.isFinished) {
                vm.processEnable.value = false
            } else {
                vm.processEnable.value = true
                val uri = outputData.getString(Constants.KEY_IMAGE_URI)
                if (!TextUtils.isEmpty(uri)) {
                    vm.blurUri.value = Uri.parse(uri)
                }
            }
        }
    })
 
    vm.saveWorkInfo.observe(this, Observer {
        saveImageUri = ""
        if (it == null || it.isEmpty()) return@Observer
        with(it[0]) {
            saveImageUri = outputData.getString(Constants.KEY_SHOW_IMAGE_URI)
            vm.showImageEnable.value = state.isFinished && !TextUtils.isEmpty(saveImageUri)
        }
    })
 
    ......
     ......
}

再来看一个通过id获取的:

    // ViewModel
    internal val dataSourceInfo: MediatorLiveData<WorkInfo> = MediatorLiveData()
  
    private fun addSource() {
        val periodicWorkInfo = mWorkManager.getWorkInfoByIdLiveData(mPeriodicRequest.id)
        dataSourceInfo.addSource(periodicWorkInfo) {
            dataSourceInfo.value = it
        }
    }
    
    // Activity
    private fun addObserver() {
        vm.dataSourceInfo.observe(this, Observer {
            if (it == null) return@Observer
            with(it) {
                if (state == WorkInfo.State.ENQUEUED) {
                    val result = outputData.getString(Constants.KEY_DATA_SOURCE)
                    if (!TextUtils.isEmpty(result)) {
                        Toast.makeText(this@OtherWorkerActivity, result, Toast.LENGTH_LONG).show()
                    }
                }
            }
        })
    }

结合LiveData使用是不是很简单呢? WorkInfo获取的本质是通过操作Room数据库来获取。在文章的Work部分已经提到,在执行完Work任务之后传递的数据将会保存到Room数据库中。

所以WorkManager与AAC的结合度非常高,目的也是致力于为我们开发者提供一套完整的框架,同时也说明Google对AAC框架的重视。

如果你还未了解AAC,推荐你阅读我之前的文章

Room

LiveData

Lifecycle

ViewModel

最后我们将上面的几个WorkRequest结合起来执行,看下它们的最终效果:

clipboard.png

通过这篇文章,希望你能够熟悉运用WorkManager。如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!

项目地址

Android精华录

该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点

Android精华录

blog

查看原文

赞 8 收藏 6 评论 0

午后一小憩 发布了文章 · 2019-07-31

Paging在RecyclerView中的应用,有这一篇就够了

前言

AAC是非常不错的一套框架组件,如果你还未进行了解,推荐你阅读我之前的系列文章:

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

经过一年的发展,AAC又推出了一系列新的组件,帮助开发者更快的进行项目框架的构建与开发。这次主要涉及的是对Paging运用的全面介绍,相信你阅读了这篇文章之后将对Paging的运用了如指掌。

Paging专注于有大量数据请求的列表处理,让开发者无需关心数据的分页逻辑,将数据的获取逻辑完全与ui隔离,降低项目的耦合。

但Paging的唯一局限性是,它需要与RecyclerView结合使用,同时也要使用专有的PagedListAdapter。这是因为,它会将数据统一封装成一个PagedList对象,而adapter持有该对象,一切数据的更新与变动都是通过PagedList来触发。

这样的好处是,我们可以结合LiveData或者RxJava来对PagedList对象的创建进行观察,一旦PagedList已经创建,只需将其传入给adapter即可,剩下的数据操更新操作将由adapter自动完成。相比于正常的RecyclerView开发,简单了许多。

下面我们通过两个具体实例来对Paging进行了解

  1. Database中的使用
  2. 自定义DataSource

Database中的使用

Paging在Database中的使用非常简单,它与Room结合将操作简单到了极致,我这里将其归纳于三步。

  1. 使用DataSource.Factory来获取Room中的数据
  2. 使用LiveData来观察PagedList
  3. 使用PagedListAdapter来与数据进行绑定与更新

DataSource.Factory

首先第一步我们需要使用DataSource.Factory抽象类来获取Room中的数据,它内部只要一个create抽象方法,这里我们无需实现,Room会自动帮我们创建PositionalDataSource实例,它将会实现create方法。所以我们要做的事情非常简单,如下:

@Dao
interface ArticleDao {
 
    // PositionalDataSource
    @Query("SELECT * FROM article")
    fun getAll(): DataSource.Factory<Int, ArticleModel>
}

我们只需拿到实现DataSource.Factory抽象的实例即可。

第一步就这么简单,接下来看第二步

LiveData

现在我们在ViewMode中调用上面的getAll方法获取所有的文章信息,并且将返回的数据封装成一个LiveData,具体如下:

class PagingViewModel(app: Application) : AndroidViewModel(app) {
    private val dao: ArticleDao by lazy { AppDatabase.getInstance(app).articleDao() }
 
    val articleList = dao.getAll()
            .toLiveData(Config(
                    pageSize = 5
            ))
}

通过DataSource.Factory的toLiveData扩展方法来构建PagedList的LiveData数据。其中Config中的参数代表每页请求的数据个数。

我们已经拿到了LiveData数据,接下来进入第三步

PagedListAdapter

前面已经说了,我们要实现PagedListAdapter,并将第二步拿到的数据传入给它。

PagedListAdapter与RecyclerView.Adapter的使用区别不大,只是对getItemCount与getItem进行了重写,因为它使用到了DiffUtil,避免对数据的无用更新。

class PagingAdapter : PagedListAdapter<ArticleModel, PagingVH>(diffCallbacks) {
 
    companion object {
        private val diffCallbacks = object : DiffUtil.ItemCallback<ArticleModel>() {

            override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem.id == newItem.id
 
            override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem == newItem

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingVH = PagingVH(R.layout.item_paging_article_layout, parent)
 
    override fun onBindViewHolder(holder: PagingVH, position: Int) = holder.bind(getItem(position))
}

这样adapter也已经构建完成,最后一旦PagedList被观察到,使用submitList传入到adapter即可。

viewModel.articleList.observe(this, Observer {
    adapter.submitList(it)
})

clipboard.png

一个基于Paging的Database列表已经完成,是不是非常简单呢?如果需要完整代码可以查看Github

自定义DataSource

上面是通过Room来获取数据,但我们需要知道的是,Room之所以简单是因为它会帮我们自己实现许多数据库相关的逻辑代码,让我们只需关注与自己业务相关的逻辑即可。而这其中与Paging相关的是对DataSource与DataSource.Factory的具体实现。

但是我们实际开发中数据绝大多数来自于网络,所以DataSource与DataSource.Factory的实现还是要我们自己来啃。

所幸的是,对于DataSource的实现,Paging已经帮我们提供了三个非常全面的实现,分别是:

  1. PageKeyedDataSource: 通过当前页相关的key来获取数据,非常常见的是key作为请求的page的大小。
  2. ItemKeyedDataSource: 通过具体item数据作为key,来获取下一页数据。例如聊天会话,请求下一页数据可能需要上一条数据的id。
  3. PositionalDataSource: 通过在数据中的position作为key,来获取下一页数据。这个典型的就是上面所说的在Database中的运用。

PositionalDataSource相信已经有点印象了吧,Room中默认帮我实现的就是通过PositionalDataSource来获取数据库中的数据的。

接下来我们通过使用最广的PageKeyedDataSource来实现网络数据。

基于Databases的三步,我们这里将它的第一步拆分为两步,所以我们只需四步就能实现Paging对网络数据的处理。

  1. 基于PageKeyedDataSource实现网络请求
  2. 实现DataSource.Factory
  3. 使用LiveData来观察PagedList
  4. 使用PagedListAdapter来与数据进行绑定与更

PageKeyedDataSource

我们自定义的DataSource需要实现PageKeyedDataSource,实现了之后会有如下三个方法需要我们去实现

class NewsDataSource(private val newsApi: NewsApi,
                     private val domains: String,
                     private val retryExecutor: Executor) : PageKeyedDataSource<Int, ArticleModel>() {
 
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        // 初始化第一页数据
    }
    
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // 加载下一页数据
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // 加载前一页数据
    }
}

其中loadBefore暂时用不到,因为我这个实例是获取新闻列表,所以只需要loadInitial与loadAfter即可。

至于这两个方法的具体实现,其实没什么多说的,根据你的业务要求来即可,这里要说的是,数据获取完毕之后要回调方法第二个参数callback的onResult方法。例如loadInitial:

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        initStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, 1, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    override fun onComplete() {
                    }
 
                    override fun onError(e: Throwable) {
                        retry = {
                            loadInitial(params, callback)
                        }
                        initStatus.postValue(Error(e.localizedMessage))
                    }

                    override fun onNext(t: ArticleListModel) {
                        initStatus.postValue(Success(200))
                        callback.onResult(t.articles, 1, 2)
                    }
                }))
    }

在onNext方法中,我们将获取的数据填充到onResult方法中,同时传入了之前的页码previousPageKey(初始化为第一页)与之后的页面nextPageKey,nextPageKey自然是作用于loadAfter方法。这样我们就可以在loadAfter中的params参数中获取到:

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        loadStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, params.key, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    override fun onComplete() {
                    }
 
                    override fun onError(e: Throwable) {
                        retry = {
                            loadAfter(params, callback)
                        }
                        loadStatus.postValue(Error(e.localizedMessage))
                    }
 
                    override fun onNext(t: ArticleListModel) {
                        loadStatus.postValue(Success(200))
                        callback.onResult(t.articles, params.key + 1)
                    }
                }))
    }

这样DataSource就基本上完成了,接下来要做的是,实现DataSource.Factory来生成我们自定义的DataSource

DataSource.Factory

之前我们就已经提及到,DataSource.Factory只有一个abstract方法,我们只需实现它的create方法来创建自定义的DataSource即可:

class NewsDataSourceFactory(private val newsApi: NewsApi,
                            private val domains: String,
                            private val executor: Executor) : DataSource.Factory<Int, ArticleModel>() {
 
    val dataSourceLiveData = MutableLiveData<NewsDataSource>()
 
    override fun create(): DataSource<Int, ArticleModel> {
        val dataSource = NewsDataSource(newsApi, domains, executor)
        dataSourceLiveData.postValue(dataSource)
        return dataSource
    }
}

嗯,代码就是这么简单,这一步也就完成了,接下来要做的是将pagedList进行LiveData封装。

Repository & ViewModel

这里与Database不同的是,并没有直接在ViewModel中通过DataSource.Factory来获取pagedList,而是进一步使用Repository进行封装,统一通过sendRequest抽象方法来获取NewsListingModel的封装结果实例。

data class NewsListingModel(val pagedList: LiveData<PagedList<ArticleModel>>,
                            val loadStatus: LiveData<LoadStatus>,
                            val refreshStatus: LiveData<LoadStatus>,
                            val retry: () -> Unit,
                            val refresh: () -> Unit)
 
sealed class LoadStatus : BaseModel()
data class Success(val status: Int) : LoadStatus()
data class NoMore(val content: String) : LoadStatus()
data class Loading(val content: String) : LoadStatus()
data class Error(val message: String) : LoadStatus()

所以Repository中的sendRequest返回的将是NewsListingModel,它里面包含了数据列表、加载状态、刷新状态、重试与刷新请求。

class NewsRepository(private val newsApi: NewsApi,
                     private val domains: String,
                     private val executor: Executor) : BaseRepository<NewsListingModel> {
 
    override fun sendRequest(pageSize: Int): NewsListingModel {
        val newsDataSourceFactory = NewsDataSourceFactory(newsApi, domains, executor)
        val newsPagingList = newsDataSourceFactory.toLiveData(
                pageSize = pageSize,
                fetchExecutor = executor
        )
        val loadStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
            it.loadStatus
        }
        val initStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
            it.initStatus
        }
        return NewsListingModel(
                pagedList = newsPagingList,
                loadStatus = loadStatus,
                refreshStatus = initStatus,
                retry = {
                    newsDataSourceFactory.dataSourceLiveData.value?.retryAll()
                },
                refresh = {
                    newsDataSourceFactory.dataSourceLiveData.value?.invalidate()
                }
        )
    }

}

接下来ViewModel中就相对来就简单许多了,它需要关注的就是对NewsListingModel中的数据进行分离成单个LiveData对象即可,由于本身其成员就是LiveDate对象,所以分离也是非常简单。分离是为了以便在Activity进行observe观察。

class NewsVM(app: Application, private val newsRepository: BaseRepository<NewsListingModel>) : AndroidViewModel(app) {

    private val newsListing = MutableLiveData<NewsListingModel>()
 
    val adapter = NewsAdapter {
        retry()
    }
 
    val newsLoadStatus = Transformations.switchMap(newsListing) {
        it.loadStatus
    }
 
    val refreshLoadStatus = Transformations.switchMap(newsListing) {
        it.refreshStatus
    }
 
    val articleList = Transformations.switchMap(newsListing) {
        it.pagedList
    }
 
    fun getData() {
        newsListing.value = newsRepository.sendRequest(20)
    }
 
    private fun retry() {
        newsListing.value?.retry?.invoke()
    }
 
    fun refresh() {
        newsListing.value?.refresh?.invoke()
    }
}

PagedListAdapter & Activity

Adapter部分与Database的基本类似,主要也是需要实现DiffUtil.ItemCallback,剩下的就是正常的Adapter实现,我这里就不再多说了,如果需要的话请阅读源码

最后的observe代码

    private fun addObserve() {
        newsVM.articleList.observe(this, Observer {
            newsVM.adapter.submitList(it)
        })
        newsVM.newsLoadStatus.observe(this, Observer {
            newsVM.adapter.updateLoadStatus(it)
        })
        newsVM.refreshLoadStatus.observe(this, Observer {
            refresh_layout.isRefreshing = it is Loading
        })
        refresh_layout.setOnRefreshListener {
            newsVM.refresh()
        }
        newsVM.getData()
    }

clipboard.png

Paging封装的还是非常好的,尤其是项目中对RecyclerView非常依赖的,还是效果不错的。当然它的优点也是它的局限性,这一点也是没办法的事情。

希望你通过这篇文章能够熟悉运用Paging,如果这篇文章对你有所帮助,你可以顺手关注一波,这是对我最大的鼓励!

项目地址

Android精华录

该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点

Android精华录

blog

查看原文

赞 5 收藏 5 评论 0

午后一小憩 发布了文章 · 2019-07-17

只需三步实现Databinding插件化

0?wx_fmt=jpeg

首先为何我要实现Databinding这个小插件,主要是在日常开发中,发现每次通过Android Studio的Layout resource file来创建xml布局文件时,布局文件的格式都没有包含Databinding所要的标签<layout>。导致的问题就是每次都要重复手动修改布局文件,添加<layout>标签等。

所以为了能够偷懒,就有个这个一步生成符合Databinding的布局文件。

这篇文章不会详细讲每一个代码的实现,因为这样太浪费大家的时间,我会通过几个要点与关键代码来梳理实现过程,而且感兴趣的之后再去看源码也会很容易理解。

源码地址(欢迎来这点击start😁):

https://github.com/idisfkj/da...

废话不多说,先来看下这个插件的效果

640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1

三步走

实现上面的插件,我这里归纳为三步,只要你掌握了这三步,你也能够实现自己的插件,提高日常开发,减少不必要的重复操作。

  1. 创建Actions
  2. 生成Panel布局
  3. 配置持久化Component

创建Actions

至于如何使用Gradle来创建plugin项目,这不是今天的主题,所以就不多介绍了。我这里提供一个链接,可以帮助你快速使用Gradle创建plugin项目

http://www.jetbrains.org/inte...

就如上面的gif效果图一样,首先第一步是通过layout文件节点,弹出菜单列表,最后在New选项子列表中呈现Databinding layout resource file选项。如下图所示

clipboard.png

上面的这整个步骤,可以归纳为一点,就是Action,所以我们接下来需要自定义Action。

但所幸的是intellij openapi已经为我们提供了AnAction类,我们要做的只需继承它,来实现具体的update与actionPerformed方法即可。

config

在实现方法之前,我们需要在resources/META-INF/plugin.xml文件中进行配置。

    <actions>
        <!-- Add your actions here -->
        <action class="com.idisfkj.databinding.autorun.actions.DataBindingAutorunAction"
                id="DataBindingAutorunAction"
                text="_DataBinding layout resource file"
                description="Create DataBinding Resource File">
            <add-to-group group-id="NewGroup" anchor="first"/>
        </action>
    </actions>

该配置最重要的是最后一条add-to-group,这里我们需要将当前Action添加到NewGroup的系统列表中,这样我们才能在上图中的New的扩展列表中看到Databinding layout resources file选项。

原则上我们在AS能够看到的列表,都能够进行插入。例如顶部的File、Edit、View等菜单栏,同时也可以创建新的顶部菜单栏。

clipboard.png

update

这个方法主要是用来更新Action的状态,它的回调会非常频繁与迅速。通过这个回调方法来控制Databinding layout resource file这个选项的显隐。

为什么要控制显隐呢?很简单,一方面我们创建.xml资源文件只能在layout文件夹下,所以我们要控制它的创建位置;另一方面也是为了与原生的Layout resource file选项保持一致,不至于违和。

而Action的显隐是可以通过presentation.isVisible来控制。

那么最终效果与控制量都知道了,最后我们要做的就是逻辑判断。我们直接来Look at the code

    override fun update(e: AnActionEvent) {
        with(e) {
            // 默认不显示
            presentation.isVisible = false
            // AnActionEvent的扩展方法,目的是找到当前操作的虚拟文件
            handleVirtualFile { project, virtualFile ->
                // 找到当前module,并且定位到layout文件目录
                ModuleUtil.findModuleForFile(virtualFile, project)?.sourceRoots?.map {
                    val layout = PsiManager.getInstance(project)
                        .findDirectory(it)
                        ?.findSubdirectory("layout")
 
                    // 当前操作范围在layout节点下
                    if (layout != null && virtualFile.path.contains(layout.virtualFile.path)) {
                        // 显示
                        presentation.isVisible = true
                        return@map
                    }
                }
            }
        }
    }

这里有两个知识点

  1. VirtualFile: 简单的来说可以理解为项目中的文件与文件夹。 这里通过它来定位当前所处的module。更多信息可以查看下面的链接:

http://www.jetbrains.org/inte...

  1. PsiManager:项目结构管理器,这里通过它来找到layout文件目录,后续还会使用它来实现自动添加文件。更多信息可以查看下面的链接:

http://www.jetbrains.org/inte...

actionPerformed

现在我们已经控制了Action的显隐,接下来我们要做的就是实现它的点击事件。

逻辑很简单,就是一个简单的点击事件,弹出一个编辑框。

    override fun actionPerformed(e: AnActionEvent) {
        // AnActionEvent的扩展方法,目的是找到当前操作的虚拟文件
        e.handleVirtualFile { project, virtualFile ->
            NewLayoutDialog(project, virtualFile).show()
        }
    }

重点是NewLayoutDialog的内部处理逻辑,那么我们继续。

生成Panel布局

现在我们要做的是

  1. 创建Dialog弹窗
  2. 绘制弹窗布局
  3. 实现点击事件
  4. 创建资源布局文件

clipboard.png

创建Dialog弹窗

对于Dialog弹窗的创建也是非常方便的,只需继承DialogWrapper。在初始化时调用它的init方法,之后就是实现具体的布局createCenterPanel与点击事件doOKAction方法。

    init {
        title = "New DataBinding Layout Resource File"
        init()
    }
 
    override fun createCenterPanel(): JComponent? = panel
 
    override fun doOKAction() {}

绘制弹窗布局

如果使用传统的GUI布局,个人感觉非常麻烦。因为项目使用的是kotlin,所以我这里使用了Kotlin UI DSL,如果你不了解的话可以查看下面的链接。

http://www.jetbrains.org/inte...

要实现上述的布局效果,需要继承JPanel,然后添加两个文本label与输入框JTextField。具体如下

class NewLayoutPanel(project: Project) : JPanel() {
 
    val fileName = JTextField()
    val rootElement = JTextField()
 
    init {
        layout = BorderLayout()
        val panel = panel(LCFlags.fill) {
            row("File name:") { fileName() }
            row("Root element:") { rootElement() }
        }
        rootElement.text = SettingsComponent.getInstance(project).defaultRootElement
 
        add(panel, BorderLayout.CENTER)
    }
 
    override fun getPreferredSize(): Dimension = Dimension(300, 40)
}

代码中的SettingsComponent是用来保存持久化配置的,而这里是获取设置页面配置的数据,后续会提及到。

现在已经有了布局,再将自定义的布局添加到createCenterPanel方法中。接下来要做的是实现弹窗的OK点击

实现点击事件

点击的逻辑是,首先查看当前将要创建的文件名称是否已经存在,其次才是创建文件,添加到目录中。

对于文件名称是否重名,开始我是通过查找该目录下的所有文件来进行判断的,但后来发现无需这么麻烦。因为在添加文件的时候会进行自动判断,如果有重名会抛出异常,所以可以通过捕获异常来进行弹窗提示。

文件的创建通过PsiFileFactory的createFileFromText方法

val file = PsiFileFactory.getInstance(project)
    .createFileFromText(
        (panel.fileName.text
            ?: TemplateUtils.TEMPLATE_DATABINDING_FILE_NAME) + TemplateUtils.TEMPLATE_LAYOUT_SUFFIX,
        XMLLanguage.INSTANCE,
        TemplateUtils.getTemplateContent(panel.rootElement.text)
    )

三个参数值分别为

  • 文件名: 通过布局panel获取text
  • 语言: 因为是.xml布局文件,所用是xml语言
  • 内容: 这里使用了预先定制的模板(可任意修改)

接下来就是将文件添加到layout下,这里还是要使用之前的PsiManager来定位到layout目录下

// 通过Swing dispatch thread来进行写操作
ApplicationManager.getApplication().runWriteAction {
    // module的扩展方法,目的是通过PsiManager定位到layout目录下
    getModule()?.handleVirtualFile {
        // 判断该操作是否在可接受的范围内
        if (actionVirtualFile.path.contains(it.virtualFile.path)) {
            try {
                // 添加文件
                it.add(file)
                // 关闭弹窗
                close(OK_EXIT_CODE)
            } catch (e: IncorrectOperationException) {
                // 异常弹窗提醒
                NotificationUtils.showMessage(
                    project, "error",
                    e.localizedMessage
                )
                e.printStackTrace()
            }
        }
    }
}

现在,如果你将要创建的文件存在重名,将会弹出如下提示

clipboard.png

当然如果成功,文件就已经创建在layout目录下,同时是Databinding模式的xml文件。

配置持久化Component

其实到这里基本已经可以正常使用了,但为了该插件能更灵活点,我还是增加了配置功能。

clipboard.png

这是插件的设置页面,我在这里提供了Default Root Element的设置,它是创建xml文件的布局根节点标签,默认是LinearLayout,所以你可以通过修改它来改变每次弹窗的默认根布局节点标签。

当然这只是一个小功能,在这里提出是为了让大家了解设置页的实现。

之前我还实现了可以自定义xml的内容模板,但后来想意义并不大就删除掉了,因为我们日常开发中布局的内容都是多变的,唯一能稍微固定的也就是布局的根节点了。

Setting布局

对于设置页的布局,其实也是一个label与JTextField,所以我这里就不多说了,具体可以查看源码

Configurable

设置页需要实现Configurable接口,它会提供是4个方法

    override fun isModified(): Boolean = modified
 
    override fun getDisplayName(): String = "DataBinding Autorun"
 
    override fun apply() {
        SettingsComponent.getInstance(project).defaultRootElement = settingsPanel.defaultRootElement.text
        modified = false
    }
 
    override fun createComponent(): JComponent? = settingsPanel.apply {
        defaultRootElement.text = SettingsComponent.getInstance(project).defaultRootElement
        defaultRootElement.document.addDocumentListener(this@SettingsConfigurable)
    }
  • isModified: 是否进行了修改,为true的话设置页的Apply就会变成可点击
  • getDisplayName: 在Android Studio的OtherSettings中展示的名称
  • apply: Apply的点击回调
  • createComponent: 布局

对于isModified的判断逻辑,引入对document的监听DocumentListener

    override fun changedUpdate(e: DocumentEvent?) {
        modified = true
    }
 
    override fun insertUpdate(e: DocumentEvent?) {
        modified = true
    }
 
    override fun removeUpdate(e: DocumentEvent?) {
        modified = true
    }

它提供的三个方法只要发生了回调,就认为是编辑了该设置页。

最后在apply与createComponent中都用到了SettingsComponent,它是用来保存数据的,保证设置的defaultRootElement能够实时保存,类似于Android的sharedpreferences

PersistentStateComponent

要实现数据的持久话,需要实现PersistentStateComponent接口。它会暴露getState与loadState两个方法,让我们来获取与保存状态。

它的保存方式也是通过.xml的文件方式进行保存,所以需要使用@state来进行配置,具体如下

@State(
    name = "SettingsConfiguration",
    storages = [Storage(value = "settingsConfiguration.xml")]
)
class SettingsComponent : PersistentStateComponent<SettingsComponent> {
 
    var defaultRootElement = "LinearLayout"
 
    companion object {
        fun getInstance(project: Project): SettingsComponent =
            ServiceManager.getService(project, SettingsComponent::class.java)
    }
 
    override fun getState(): SettingsComponent? = this
 
    override fun loadState(state: SettingsComponent) {
        XmlSerializerUtil.copyBean(state, this)
    }
}

该状态名为SettingConfiguration,保存在settingConfiguration.xml文件中。保存方式会借助XmlSerializerUtil来实现。

当然为了保存该实例的单例模式,这里使用ServiceManager的getService方法来获取它的实例。所以在上面的Configurable中,使用的就是这个方式。

配置

自定义的SettingsConfigurable与SettingsComponent都需要到plugin.xml中进行配置,这与之前的Action类似。你可以理解为Android的四大组件。

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
        <defaultProjectTypeProvider type="Android"/>
        <projectConfigurable instance="com.idisfkj.databinding.autorun.ui.settings.SettingsConfigurable"/>
        <projectService serviceInterface="com.idisfkj.databinding.autorun.component.SettingsComponent"
                        serviceImplementation="com.idisfkj.databinding.autorun.component.SettingsComponent"/>
    </extensions>
 
    <project-components>
        <component>
            <implementation-class>
                com.idisfkj.databinding.autorun.component.SettingsComponent
            </implementation-class>
        </component>
    </project-components>

由于SettingsComponent是project级别的,所以这里包含在project-components标签中;另一方面SettingsConfigurable在配置中统一归于extensions标签,至于为什么,这就涉及到扩展了,简单的说就是别人可以在你的插件基础上进行不同程度的扩展,就是基于这个的。由于这又是另外一个话题,所以就不多说了,感兴趣的可以自己去了解。

结语

关于Databinding插件化的定制就到这里了,源码已经在文章开头给出。

或者你也可以通过Android精华录获取

如果你对该插件有别的建议,欢迎@我;亦或者你在使用的过程中有什么不便的地方也可以在github中提issue,我也会第一时间进行优化。

自荐

私人独家博客: https://www.rousetime.com

技术公众号:Android补给站

clipboard.png

查看原文

赞 1 收藏 1 评论 0

午后一小憩 发布了文章 · 2019-06-28

What? 你还不知道Kotlin Coroutine?

clipboard.png

今天我们来聊聊Kotlin Coroutine,如果你还没有了解过,那么我要提前恭喜你,因为你将掌握一个新技能,对你的代码方面的提升将是很好的助力。

What Coroutine

简单的来说,Coroutine是一个并发的设计模式,你能通过它使用更简洁的代码来解决异步问题。

例如,在Android方面它主要能够帮助你解决以下两个问题:

  1. 在主线程中执行耗时任务导致的主线程阻塞,从而使App发生ANR。
  2. 提供主线程安全,同时对来自于主线程的网络回调、磁盘操提供保障。

这些问题,在接下来的文章中我都会给出解决的示例。

Callback

说到异步问题,我们先来看下我们常规的异步处理方式。首先第一种是最基本的callback方式。

callback的好处是使用起来简单,但你在使用的过程中可能会遇到如下情形

        GatheringVoiceSettingRepository.getInstance().getGeneralSettings(RequestLanguage::class.java)
                .observe(this, { language ->
                    convertResult(language, { enable -> 
                        // todo something
                    })
                })

这种在其中一个callback中回调另一个callback回调,甚至更多的callback都是可能存在。这些情况导致的问题是代码间的嵌套层级太深,导致逻辑嵌套复杂,后续的维护成本也要提高,这不是我们所要看到的。

那么有什么方法能够解决呢?当然有,其中的一种解决方法就是我接下来要说的第二种方式。

Rx系列

对多嵌套回调,Rx系列在这方面处理的已经非常好了,例如RxJava。下面我们来看一下RxJava的解决案例

        disposable = createCall().map {
            // return RequestType
        }.subscribeWith(object : SMDefaultDisposableObserver<RequestType>{
            override fun onNext(t: RequestType) {
                // todo something
            }
        })

RxJava丰富的操作符,再结合Observable与Subscribe能够很好的解决异步嵌套回调问题。但是它的使用成本就相对提高了,你要对它的操作符要非常了解,避免在使用过程中滥用或者过度使用,这样自然复杂度就提升了。

那么我们渴望的解决方案是能够更加简单、全面与健壮,而我们今天的主题Coroutine就能够达到这种效果。

Coroutine在Kotlin中的基本要点

在Android里,我们都知道网络请求应该放到子线程中,相应的回调处理一般都是在主线程,即ui线程。正常的写法就不多说了,那么使用Coroutine又该是怎么样的呢?请看下面代码示例:

    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
        // to do network request
        url
    }
 
    private suspend fun fetch() { // 在Main中调用
        val result = get("https://rousetime.com") // 在IO中调用
        showToast(result) // 在Main中调用
    }

如果fetch方法在主线程调用,那么你会发现使用Coroutine来处理异步回调就像是在处理同步回调一样,简洁明了、行云流水,同时再也没有嵌套的逻辑了。

注意看方法,Coroutine为了能够实现这种简单的操作,增加了两个操作来解决耗时任务,分别为suspend与resume

  • suspend: 挂起当前执行的协同程序,并且保存此刻的所有本地变量
  • resume: 从它被挂起的位置继续执行,并且挂起时保存的数据也被还原

解释的有点生硬,简单的来说就是suspend可以将该任务挂起,使它暂时不在调用的线程中,以至于当前线程可以继续执行别的任务,一旦被挂起的任务已经执行完毕,那么就会通过resume将其重新插入到当前线程中。

所以上面的示例展示的是,当get还在请求的时候,fetch方法将会被挂起,直到get结束,此时才会插入到主线程中并返回结果。

一图胜千言,我做了一张图,希望能有所帮助。

clipboard.png

另外需要注意的是,suspend方法只能够被其它的suspend方法调用或者被一个coroutine调用,例如launch。

Dispatchers

另一方面Coroutine使用Dispatchers来负责调度协调程序执行的线程,这一点与RxJava的schedules有点类似,但不同的是Coroutine一定要执行在Dispatchers调度中,因为Dispatchers将负责resume被suspend的任务。

Dispatchers提供三种模式切换,分别为

  1. Dispatchers.Main: 使Coroutine运行中主线程,以便UI操作
  2. Dispatchers.IO: 使Coroutine运行在IO线程,以便执行网络或者I/O操作
  3. Dispatchers.Default: 在主线程之外提高对CPU的利用率,例如对list的排序或者JSON的解析。

再来看上面的示例

    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
        // to do network request
        url
    }
 
    private suspend fun fetch() { // 在Main中调用
        val result = get("https://rousetime.com") // 在IO中调用
        showToast(result) // 在Main中调用
    }

为了让get操作运行在IO线程,我们使用withContext方法,对该方法传入Dispatchers.IO,使得它闭包下的任务都处于IO线程中,同时witchContext也是一个suspend函数。

创建Coroutine

上面提到suspend函数只能在相应的suspend中或者Coroutine中调用。那么Coroutine又该如何创建呢?

有两种方式,分别为launch与async

  1. launch: 开启一个新的Coroutine,但不返回结果
  2. async: 开启一个新的Coroutine,但返回结果

还是上面的例子,如果我们需要执行fetch方法,可以使用launch创建一个Coroutine

    private fun excute() {
        CoroutineScope(Dispatchers.Main).launch {
            fetch()
        }
    }

另一种async,因为它返回结果,如果要等所有async执行完毕,可以使用await或者awaitAll

    private suspend fun fetchAll() {
        coroutineScope {
            val deferredFirst = async { get("first") }
            val deferredSecond = async { get("second") }
            deferredFirst.await()
            deferredSecond.await()

//            val deferred = listOf(
//                    async { get("first") },
//                    async { get("second") }
//            )
//            deferred.awaitAll()
        }
    }

所以通过await或者awaitAll可以保证所有async完成之后再进行resume调用。

Architecture Components

如果你使用了Architecture Component,那么你也可以在其基础上使用Coroutine,因为Kotlin Coroutine已经提供了相应的api并且定制了CoroutineScope。

如果你还不了解Architecture Component,强烈推荐你阅读我的Android Architecture Components 系列

在使用之前,需要更新architecture component的依赖版本,如下所示

object Versions {
    const val arch_version = "2.2.0-alpha01"
    const val arch_room_version = "2.1.0-rc01"
}
 
object Dependencies {
    val arch_lifecycle = "androidx.lifecycle:lifecycle-extensions:${Versions.arch_version}"
    val arch_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.arch_version}"
    val arch_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.arch_version}"
    val arch_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.arch_version}"
    val arch_room_runtime = "androidx.room:room-runtime:${Versions.arch_room_version}"
    val arch_room_compiler = "androidx.room:room-compiler:${Versions.arch_room_version}"
    val arch_room = "androidx.room:room-ktx:${Versions.arch_room_version}"
}

ViewModelScope

在ViewModel中,为了能够使用Coroutine提供了viewModelScope.launch,同时一旦ViewModel被清除,对应的Coroutine也会自动取消。

    fun getAll() {
        viewModelScope.launch {
            val articleList = withContext(Dispatchers.IO) {
                articleDao.getAll()
            }
            adapter.clear()
            adapter.addAllData(articleList)
        }
    }

在IO线程通过articleDao从数据库取数据,一旦数据返回,在主线程进行处理。如果在取数据的过程中ViewModel已经清除了,那么数据获取也会停止,防止资源的浪费。

LifecycleScope

对于Lifecycle,提供了LifecycleScope,我们可以直接通过launch来创建Coroutine

    private fun coroutine() {
        lifecycleScope.launch {
            delay(2000)
            showToast("coroutine first")
            delay(2000)
            showToast("coroutine second")
        }
    }

因为Lifecycle是可以感知组件的生命周期的,所以一旦组件onDestroy了,相应的LifecycleScope.launch闭包中的调用也将取消停止。

lifecycleScope本质是Lifecycle.coroutineScope

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
 
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }

它会在onStateChanged中监听DESTROYED状态,同时调用cancel取消Coroutine。

另一方面,lifecycleScope还可以根据Lifecycle不同的生命状态进行suspend处理。例如对它的STARTED进行特殊处理

    private fun coroutine() {
        lifecycleScope.launchWhenStarted {

        }
        lifecycleScope.launch {
            whenStarted {  }
            delay(2000)
            showToast("coroutine first")
            delay(2000)
            showToast("coroutine second")
        }
    }

不管是直接调用launchWhenStarted还是在launch中调用whenStarted都能达到同样的效果。

LiveData

LiveData中可以直接使用liveData,在它的参数中会调用一个suspend函数,同时会返回LiveData对象

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

所以我们可以直接使用liveData来是实现Coroutine效果,我们来看下面一段代码

    // Room
    @Query("SELECT * FROM article_model WHERE title = :title LIMIT 1")
    fun findByTitle(title: String): ArticleModel?
    // ViewModel
    fun findByTitle(title: String) = liveData(Dispatchers.IO) {
        MyApp.db.articleDao().findByTitle(title)?.let {
            emit(it)
        }
    }
    // Activity
    private fun checkArticle() {
        vm.findByTitle("Android Architecture Components Part1:Room").observe(this, Observer {
        })
    }

通过title从数据库中取数据,数据的获取发生在IO线程,一旦数据返回,再通过emit方法将返回的数据发送出去。所以在View层,我们可以直接使用checkArticle中的方法来监听数据的状态。

另一方面LiveData有它的active与inactive状态,对于Coroutine也会进行相应的激活与取消。对于激活,如果它已经完成了或者非正常的取消,例如抛出CancelationException异常,此时将不会自动激活。

对于发送数据,还可以使用emitSource,它与emit共同点是在发送新的数据之前都会将原数据清除,而不同点是,emitSource会返回一个DisposableHandle对象,以便可以调用它的dispose方法进行取消发送。

最后我使用Architecture Component与Coroutine写了个简单的Demo,大家可以在Github中进行查看

源码地址: https://github.com/idisfkj/an...

推荐阅读

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

公众号

扫描二维码,关注微信公众号,获取独家最新IT技术!

clipboard.png

查看原文

赞 7 收藏 5 评论 1

午后一小憩 发布了文章 · 2019-06-10

Android Gradle系列-进阶篇

clipboard.png

上篇文章我们已经将Gradle基础运用介绍了一遍,可以这么说,只要你一直看了我这个Gradle系列,那么你的Gradle也将过关了,应对正常的工作开发已经不成问题了。

这篇文章我要向你介绍的是关于如何使用Gradle来更加优雅的管理多个module之间的依赖关系。

相信你一定有这样的经历:主项目依赖于多个子项目,或者项目间互相依赖。不同子项目间的依赖的第三方库版本又没有进行统一,升级一个版本所有依赖的项目都要进行修改;甚至minSdkVersion与targetSdkVersion也不相同。

今天我们就来解决这个问题,让Gradle版本管理更加优雅。

Google推荐

之前的文章Android Gradle系列-运用篇中的dependencies使用的是最基本的引用方式。如果你有新建一个kotlin项目的经历,那么你将看到Google推荐的方案

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

在rootProject的build.gradle中使用ext来定义版本号全局变量。这样我们就可以在module的build.gradle中直接引用这些定义的变量。引用方式如下:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}

你可以将这些变量理解为java的静态变量。通过这种方式能够达到不同module中的配置统一,但局限性是,一但配置项过多,所有的配置都将写到rootProject项目的build.gradle中,导致build.gradle臃肿。这不符合我们的所提倡的模块开发,所以应该想办法将ext的配置单独分离出来。

这个时候我就要用到之前的文章Android Gradle系列-原理篇中所介绍的apply函数。之前的文章我们只使用了apply三种情况之一的plugin(应用一个插件,通过id或者class名),只使用在子项目的build.gradle中。

apply plugin: 'com.android.application'

这次我们需要使用它的from,它主要是的作用是应用一个脚本文件。作用接下来我们需要做的是将ext配置单独放到一个gradle脚本文件中。

首先我们在rootProject目录下创建一个gradle脚本文件,我这里取名为version.gradle。

然后我们在version.gradle文件中使用ext来定义变量。例如之前的kotlin版本号就可以使用如下方式实现

ext.deps = [:]
 
def versions = [:]
versions.support = "26.1.0"
versions.kotlin = "1.2.51"
versions.gradle = '3.2.1'
 
def support = [:]
support.app_compat = "com.android.support:appcompat-v7:$versions.support"
support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
deps.support = support
 
def kotlin = [:]
kotlin.kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin"
kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
deps.kotlin = kotlin
 
deps.gradle_plugin = "com.android.tools.build:gradle:$versions.gradle"
 
ext.deps = deps
 
def build_versions = [:]
build_versions.target_sdk = 26
build_versions.min_sdk = 16
build_versions.build_tools = "28.0.3"
ext.build_versions = build_versions
 
def addRepos(RepositoryHandler handler) {
    handler.google()
    handler.jcenter()
    handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}
ext.addRepos = this.&addRepos
因为gradle使用的是groovy语言,所以以上都是groovy语法

例如kotlin版本控制,上面代码的意思就是将有个kotlin相关的版本依赖放到deps的kotlin变量中,同时deps放到了ext中。其它的亦是如此。

既然定义好了,现在我们开始引入到项目中,为了让所有的子项目都能够访问到,我们使用apply from将其引入到rootProject的build.gradle中

buildscript {
    apply from: 'versions.gradle'
    addRepos(repositories)
    dependencies {
        classpath deps.gradle_plugin
        classpath deps.kotlin.plugin
    }
}

这时build.gradle中就默认有了ext所声明的变量,使用方式就如dependencies中的引用一样。

我们再看上面的addRepos方法,在关于Gradle原理的文章中已经分析了repositories会通过RepositoryHandler来执行,所以这里我们直接定义一个方法来统一调用RepositoryHandler。这样我们在build.gradle中就无需使用如下方式,直接调用addRepos方法即可

    //之前调用
    repositories {
        google()
        jcenter()
    }
    //现在调用
    addRepos(repositories)

另一方面,如果有多个module,例如有module1,现在就可以直接在module1中的build.gradle中使用定义好的配置

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // support
    implementation deps.support.app_compat
    //kotlin
    implementation deps.kotlin.kotlin_stdlib
}

上面我们还定义了sdk与tools版本,所以也可以一起统一使用,效果如下

android {
    compileSdkVersion build_versions.target_sdk
    buildToolsVersion build_versions.build_tools
    defaultConfig {
        applicationId "com.idisfkj.androidapianalysis"
        minSdkVersion build_versions.min_sdk
        targetSdkVersion build_versions.target_sdk
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    ...
}

一旦实现了统一配置,那么之后我们要修改相关的版本就只需在我们定义的version.gradle中修改即可。无需再对所用的module进行逐一修改与统一配置。

BuildSrc&Kotlin

如果你的项目使用了kotlin,那么buildSrc&Kotlin的统一管理方案将更适合你。

Gradle项目会默认识别buildSrc目录,并且会将该目录中的配置注入到build.gradle中,以至于让build.gradle能够直接引用buildSrc中的配置项。

有了这一特性,我们就可以直接将之前version.gradle中的配置放入到buildSrc中,下面我们开始实现。

首先在根目录新建一个buildSrc目录(与app同级),然后在该目录新建src/main/java目录,该目录是你之后配置项所在的目录;同时再新建build.gradle.kts文件,并在该文件中添加kotlin-dsl

plugins {
    `kotlin-dsl`
}
 
repositories {
    jcenter()
}

之后再sync project,最终的目录结构如下

clipboard.png

搭建好了目录,现在我们在src/main/java下使用kotlin新建Dependencies文件(文件名任意),在该文件中将之前的配置项放进来,只是使用kotlin语法进行实现而已,转化的代码如下

object Versions {
    const val support = "26.1.0"
    const val kotlin = "1.3.31"
    const val gradle = "3.4.1"
    const val target_sdk = 26
    const val min_sdk = 16
    const val build_tools = "28.0.3"
}
 
object Dependencies {
    val app_compat = "com.android.support:appcompat-v7:${Versions.support}"
    val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
    val kotlin_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
    val gradle_plugin = "com.android.tools.build:gradle:${Versions.gradle}"
    val addRepos: (handler: RepositoryHandler) -> Unit = {
        it.google()
        it.jcenter()
        it.maven { url = URI("https://oss.sonatype.org/content/repositories/snapshots") }
    }
}

这时你就可以直接使用Dependencies与Versions在各个build.gradle中引用,例如app下的build.gradle

android {
    compileSdkVersion Versions.target_sdk
    buildToolsVersion Versions.build_tools
    defaultConfig {
        applicationId "com.idisfkj.androidapianalysis"
        minSdkVersion Versions.min_sdk
        targetSdkVersion Versions.target_sdk
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    ...
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // support
    implementation Dependencies.app_compat
    //kotlin
    implementation Dependencies.kotlin_stdlib
}

根目录的build.gradle亦是如此

buildscript {
    Dependencies.addRepos.invoke(repositories)
    dependencies {
        classpath Dependencies.gradle_plugin
        classpath Dependencies.kotlin_plugin
    }
}
 
allprojects {
    Dependencies.addRepos.invoke(repositories)
}
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

其实我们真正需要get到的是一种思想,将配置统一管理。至于到底使用哪一种,这就看个人喜好了,但如果你的项目使用了kotlin,我还是建议你使用buildSrc模式,因为对于Groovy语法而言,我相信你还是对Kotlin更加熟悉。

源码地址: https://github.com/idisfkj/an...

如果想了解更多关于我的文章,可以扫描下方二维码,关注我的公众号~

clipboard.png

查看原文

赞 9 收藏 9 评论 0

午后一小憩 发布了文章 · 2019-05-30

Gradle系列-运用篇

clipboard.png

上次我们说到gradle的原理,主要是偏理论上的知识点,直通车在这Android Gradle系列-原理篇。这次我们来点实战的,随便巩固下之前的知识点。

android

在app module下的gradle.build中都有一个android闭包,主要配置都在这里设置。例如默认配置项:defaultConfig;签名相关:signingConfig;构建变体:buildTypes;产品风格:productFlavors;源集配置:sourceSets等。

defaultConfig

对于defaultConfig其实它是也一个productFlavor,只不过这里是用来提供默认的设置项,如果之后的productFlavor没有特殊指定的配置都会使用defaultConfig中的默认配置。

public class DefaultConfig extends BaseFlavor {
    @Inject
    public DefaultConfig(
            @NonNull String name,
            @NonNull Project project,
            @NonNull ObjectFactory objectFactory,
            @NonNull DeprecationReporter deprecationReporter,
            @NonNull Logger logger) {
        super(name, project, objectFactory, deprecationReporter, logger);
    }
}
 
public abstract class BaseFlavor extends DefaultProductFlavor implements CoreProductFlavor {
    ...
}

可以看到defaultConfig的超级父类就是DefaultProductFlavor。而在DefaultProductFlavor中定义了许多我们经常见到的配置:VersionCode、VersionName、minSdkVersion、targetSdkVersion与applicationId等等。

有了上面的基础,那么在defaultConfig中我们要配置的变量就显而易见了。

    defaultConfig {
        applicationId "com.idisfkj.androidapianalysis"
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

signingConfigs

signingConfig是用来配置keyStore,我们可以针对不同的版本配置不同的keyStore,例如

    signingConfigs {
        config { //默认配置
            storeFile file('key.store')
            storePassword 'android123'
            keyAlias 'android'
            keyPassword 'android123'
        }
        dev { //dev测试版配置
            storeFile file('xxxx')
            storePassword 'xxx'
            keyAlias 'xxx'
            keyPassword 'xxx'
        }
    }

有人可能会说这不安全的,密码都是明文,都暴露出去了。是的,如果这项目发布到远程,那么这些秘钥就泄露出去了。所以为了安全起见,我们可以对其进一些特殊处理。

  1. 通过环境变量获取秘钥
storePassword System.getenv("KSTOREPWD")
keyPassword System.getenv("KEYPWD")
  1. 从命令行中获取秘钥
storePassword System.console().readLine("\nKeystore password: ")
keyPassword System.console().readLine("\nKey password: ")

上面两种是Android Develop官网提供的,但经过测试都会报null异常,查了下资料都说是gradle不支持(如果有成功的可以告知我),所以还是推荐下面的这种方法

在项目的根目录下(settings.gradle平级)创建keystore.properties文件,我们在这个文件中进行存储秘钥,它是支持key-value模式的键值对数据

storePassword = android123
keyPassword = android123

之后就是读取其中的password,在build.gradle通过afterEvaluate回调进行读取与设置

afterEvaluate {
    def propsFile = rootProject.file('keystore.properties')
    def configName = 'config'
    if (propsFile.exists() && android.signingConfigs.hasProperty(configName)) {
        def props = new Properties()
        props.load(new FileInputStream(propsFile))
        android.signingConfigs[configName].keyPassword = props['keyPassword']
        android.signingConfigs[configName].storePassword = props['storePassword']
    }
}

我们已经通过动态读取了password,所以在之前的signingConfigs中就无需再配置password

    signingConfigs {
        config {
            storeFile file('key.store')
            keyAlias 'android'
        }
    }

最后一步,为了保证秘钥的安全性,在.gitignore中添加keystore.properties的忽略配置,防止上传到远程仓储暴露秘钥。

buildTypes

构建变体主要用来配置shrinkResources:资源是否需要压缩、zipAlignEnabled:压缩是否对齐、minifyEnabled:是否代码混淆与signingConfig:签名配置等等。新建项目时,默认有一个release配置,但我们实际开发中可能需要多个不同的配置,例如debug模式,为了方法调试,一般都不需要对其进行代码混淆、压缩等处理。或者outer模式,需要的签名配置不同,所以最终的配置可以是这样:

    buildTypes {
        debug {
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
            signingConfig signingConfigs.config
        }
        outer {
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
            signingConfig signingConfigs.outConfig
        }
        release {
            minifyEnabled true
            zipAlignEnabled true
            shrinkResources true
            signingConfig signingConfigs.config
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

Sync Now之后,打开Android Studio 右边的Gradle,找到app->Tasks->build,发现已经添加了assembleDebug与assembleOuter构建task。

productFlavors

一个项目可能有不同的版本环境,例如开发功能中的开发版、项目上线的正式版。开发版与正式版请求的数据api可能不同,对于这种情况我们就可以使用productFlavor来构建不同的产品风格,可以看下面的dev与prod配置

    flavorDimensions "mode"
    productFlavors {
        dev {
            applicationIdSuffix ".dev"
            dimension "mode"
            manifestPlaceholders = [PROJECT_NAME: "@string/app_name_dev",
                                    APP_ID      : "21321843"]
            buildConfigField 'String', 'API_URL', '"https://dev.idisfkj.android.com"'
            buildConfigField 'String', 'APP_KEY', '"3824yk32"'
        }
        prod {
            applicationIdSuffix ".prod"
            dimension "mode"
            manifestPlaceholders = [PROJECT_NAME: "@string/app_name",
                                    APP_ID      : "12932843"]
            buildConfigField 'String', 'API_URL', '"https://prod.idisfkj.android.com"'
            buildConfigField 'String', 'APP_KEY', '"32143dsk2"'
        }
    }

对于判断是否为同一个app,手机系统是根据app的applicationId来识别的,默认applicationId是packageName。所以为了让dev与prod的版本都能共存在一个手机上,可以通过applicationIdSuffix来为applicationId增加后缀,改变安装包的唯一标识。

还有可以通过manifestPlaceholders来配置可用于AndroidManifest中的变量,例如根据不同的产品风格显示不同的app名称

dev与prod网络请求时使用不同的api host,可以设置buildConfigField,这样我们就可以在代码中通过BuildConfig获取

    fun getApiUlr(): String {
        return BuildConfig.API_URL
    }

这里的BuildConfig会根据你构建的产品风格返回不同的值,它位于build->generated->source->buildConfig->变体,大致内容如下:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.idisfkj.androidapianalysis.dev";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "devMinApi21";
  public static final int VERSION_CODE = 20001;
  public static final String VERSION_NAME = "1.0-minApi21";
  public static final String FLAVOR_mode = "dev";
  public static final String FLAVOR_api = "minApi21";
  // Fields from product flavor: dev
  public static final String API_URL = "https://dev.idisfkj.android.com";
  public static final String APP_KEY = "3824yk32";
}

Sync Now之后,打开Android Studio 右边的Gradle,找到app->Tasks->build,发现新添加了assembleDev与assembleProd构建task。

flavorDimensions是用来设置多维度的,上面的例子只展示了一个维度,所以dimension为mode的形式。我们新增一个api维度,构建不同的minSkdVerison版本的apk

    flavorDimensions "mode", "api"
    productFlavors {
        dev {
            applicationIdSuffix ".dev"
            dimension "mode"
            manifestPlaceholders = [PROJECT_NAME: "@string/app_name_dev",
                                    APP_ID      : "21321843"]
            buildConfigField 'String', 'API_URL', '"https://dev.idisfkj.android.com"'
            buildConfigField 'String', 'APP_KEY', '"3824yk32"'
        }
        prod {
            applicationIdSuffix ".prod"
            dimension "mode"
            manifestPlaceholders = [PROJECT_NAME: "@string/app_name",
                                    APP_ID      : "12932843"]
            buildConfigField 'String', 'API_URL', '"https://prod.idisfkj.android.com"'
            buildConfigField 'String', 'APP_KEY', '"32143dsk2"'
        }
        minApi16 {
            dimension "api"
            minSdkVersion 16
            versionCode 10000 + android.defaultConfig.versionCode
            versionNameSuffix "-minApi16"
        }
        minApi21 {
            dimension "api"
            minSdkVersion 21
            versionCode 20000 + android.defaultConfig.versionCode
            versionNameSuffix "-minApi21"
        }
    }

gradle创建的构建变体数量等于每个风格维度中的风格数量与你配置的构建类型数量的乘积,所以上面例子的构建变体数量为12个。在gradle为每个构建变体或对应apk命名时,属于较高优先级风格维度的产品风格首先显示,之后是较低优先级维度的产品风格,再之后是构建类型。而优先级的判断则以flavorDimensions的值顺序为依据,以上面的构建配置为例:

构建变体:dev, prod[debug, outer, release]
对应apk:app-[dev, prod]-[minApi16, minApi21]-[debug, outer, release].apk

构建变体有这么多,但有时我们并不全部需要,例如你不需要mode为dev,api为minApi16的变体,这时你就可以使用variantFilter方法来过滤

    variantFilter { variant ->
        def names = variant.flavors*.name
        if (names.contains("minApi16") && names.contains("dev")) {
            setIgnore(true)
        }
    }

你再回到app->Tasks中查看变体,会发现已经将devMinApi16相关的变体过滤了。

你不仅可以过滤构建变体,还可以改变默认的apk输出名称。例如你想修改buildType为release的apk名称,这时你可以使用android.applicationVariants.all

    android.applicationVariants.all { variant ->
        if (variant.buildType.name == buildTypes.release.name) {
            variant.outputs.all {
                outputFileName = "analysis-release-${defaultConfig.versionName}.apk"
            }
        }
    }

这样在release下的包名都是以analysis打头

sourceSets

Android Studio会帮助我们创建默认的main源集与目录(位于app/src/main),用来存储所有构建变体间的共享资源。所以你可以通过设置main源集来更改默认的配置。例如现在你想将res的路径修改成src/custom/res

    sourceSets {
        main {
            res.srcDirs = ['src/custom/res']
        }
    }

这样res资源路径就定位到了src/custom/res下,当然你也可以修改其它的配置,例如java、assets、jni等。

如果你配置了多个路径,即路径集合:

    sourceSets {
        main {
            res.srcDirs = ['src/custom/res', 'scr/main/res']
        }
    }

这时你要保证不能有相同的名称,即每个文件只能唯一存在其中一个目录下。

你也可以查看所以的构建变体的默认配置路径: 点击右边gradle->app->android->sourceSets,你将会看到如下类似信息

------------------------------------------------------------
Project :app
------------------------------------------------------------
 
androidTest
-----------
Compile configuration: androidTestCompile
build.gradle name: android.sourceSets.androidTest
Java sources: [app/src/androidTest/java]
Manifest file: app/src/androidTest/AndroidManifest.xml
Android resources: [app/src/androidTest/res]
Assets: [app/src/androidTest/assets]
AIDL sources: [app/src/androidTest/aidl]
RenderScript sources: [app/src/androidTest/rs]
JNI sources: [app/src/androidTest/jni]
JNI libraries: [app/src/androidTest/jniLibs]
Java-style resources: [app/src/androidTest/resources]
...

上面是androidTest变体的默认路径,首先它会去查找相应的构建变体的默认位置,如果没有找到,就会使用main源集下的默认配置。也就是我们所熟悉的app/src/main路径下的资源。

因为它是跟构建变体来搜索的,所以它有个优先级:

  1. src/modeApiDebug: 构建变体
  2. src/debug:构建类型
  3. src/modeApi:产品风格
  4. src/main:默认main源

对于源集的创建,如下所示在app/src下右键新建,但它只会帮你创建源集下的java文件夹,其它的都要你自己逐个创建

clipboard.png

我们自定义一个debug源集,所以进去之后Target Source Set选择debug,再点击finish结束。这时你将会在src下看到debug文件夹

现在你已经有了debug的源集目录,假设你现在要使debug下的app名称展示成Android精华录debug(默认是Android精华录)。这时你可以右键debug新建values

clipboard.png

在values目录下新建strings.xml,然后在其中配置app_name

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Android精华录debug</string>
</resources>

最后你再去构建debug相关的变体时,你安装的app展示的名称将是Android精华录debug。

所以通过修改mian源集或者配置其它的变体源集,可以实现根据变体加载不同的数据源。这样系统化的配置加载资源将更加方便项目测试与版本需要的配置。

dependencies

dependencies闭包上用来配置项目的第三方依赖,如果你根据上面的配置有设置变体,那么你将可以根据变体来选择性的依赖第三方库

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    //根据变体选择性依赖
    outerImplementation '...'
    prodMinApi21Implementation '...'
}

关于dependencies,这只是简单的配置方式,之后我还会单独抽出一篇文章来写系统化的配置dependencies,感兴趣的可以关注下。

gradle相关的配置还有很多,这里只是冰山一角,但我的建议是根据你的实际需求去学习与研究,相信你也会有意想不到的成长。

最后附上源码地址:https://github.com/idisfkj/an...

博客地址:https://www.rousetime.com/

系列

Android Gradle系列-入门篇

Android Gradle系列-原理篇

clipboard.png

查看原文

赞 8 收藏 7 评论 0

午后一小憩 发布了文章 · 2019-05-17

Android Gradle系列-原理篇

clipboard.png

上周我们在Android Gradle系列-入门篇文章中已经将gradle在项目中的结构过了一遍。对于gradle,我们许多时候都不需要修改类似与*.gradle文件,做的最多的应该是在dependencies中添加第三方依赖,或者说修改sdk版本号,亦或者每次发版本改下versionCode与versionName。即使碰到问题也是直接上google寻找答案,而并没有真正理解它为什么要这么做,或者它是如何运行的?

今天,我会通过这篇文章一步一步的编写gradle文件,从项目的创建,到gradle的配置。相信有了这篇文章,你将对gradle的内部运行将有一个全新的认识。

Groovy

在讲gradle之前,我们还需明白一点,gradle语法是基于groovy的。所以我们先来了解一些groovy的知识,这有助于我们之后的理解。当然如果你已经有groovy的基础你可以直接跳过,没有的也不用慌,因为只要你懂java就不是什么难题。

syntax

下面我将通过code的形式,列出几点

  • 当调用的方法有参数时,可以不用(),看下面的例子
def printAge(String name, int age) {
    print("$name is $age years old")
}
 
def printEmptyLine() {
    println()
}
 
def callClosure(Closure closure) {
    closure()
}
 
printAge "John", 24 //输出John is 24 years old
printEmptyLine() //输出空行
callClosure { println("From closure") } //输出From closure
  • 如果最后的参数是闭包,可以将它写在括号的外面
def callWithParam(String param, Closure<String> closure) {
    closure(param)
}
 
callWithParam("param", { println it }) //输出param
callWithParam("param") { println it } //输出param
callWithParam "param", { println it } //输出param
  • 调用方法时可以指定参数名进行传参,有指定的会转化到Map对象中,没有的将按正常传参
def printPersonInfo(Map<String, Object> person) {
    println("${person.name} is ${person.age} years old")
}
 
def printJobInfo(Map<String, Object> job, String employeeName) {
    println("${employeeName} works as ${job.name} at ${job.company}")
}
 
printPersonInfo name: "Jake", age: 29
printJobInfo "Payne", name: "Android Engineer", company: "Google"

你会发现他们的调用都不需要括号,同时printJobInfo的调用参数的顺序不受影响。

Closure

在gradle中你会发现许多闭包,所以我们需要对闭包有一定的了解。如果你熟悉kotlin,它与Function literals with receiver类似。

在groovy中我们可以将Closures当做成lambdas,所以它可以直接当做代码块执行,可以有参数,也可以有返回值。但是不同的是它可以改变其自身的代理。例如:

class DelegateOne {
    def callContent(String content) {
        println "From delegateOne: $content"
    }
}
 
class DelegateTow {
    def callContent(String content) {
        println "From delegateTwo: $content"
    }
}
 
def callClosure = {
    callContent "I am bird"
}
 
callClosure.delegate = new DelegateOne()
callClosure() //输出From delegateOne: I am bird
callClosure.delegate = new DelegateTow()
callClosure() //输出From delegateTow: I am bird

通过改变callClosure的delegate,让其调用不同的callContent。
如果你想了解更多,可以直接阅读groovy文档

Gradle

在上篇文章中已经提到有关gradle的脚步相关的知识,这里就不再累赘。
下面我们来一步一步构建gradle。

搭建项目层级

首先我们新建一个文件夹example,cd进入该文件夹,在该目录下执行gradle projects,你会发现它已经是一个gradle项目了

$ gradle projects
> Task :projects
 
------------------------------------------------------------
Root project
------------------------------------------------------------
 
Root project 'example'
No sub-projects
 
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
 
BUILD SUCCESSFUL in 5s

因为这里不是在Android Studio中创建的项目,所以如果你本地没有安装与配置gradle环境,将不会有gradle命令。所以这一点要注意一下。

每一个android项目在它的root project下都需要配置一个settings.gradle,它代表着项目的全局配置。同时使用void include(String[] projectPaths)方法来添加子项目,例如我们为example添加app子项目

$ echo "include ':app'" > settings.gradle
$ gradle projects
> Task :projects
 
------------------------------------------------------------
Root project
------------------------------------------------------------
 
Root project 'example'
\--- Project ':app'
 
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :app:tasks
 
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

:app中的:代表的是路径的分隔符,同时在settings.gradle中默认root project是该文件的文件夹名称,也可以通过rootProject.name = name来进行修改。

搭建Android子项目

现在需要做的是将子项目app构建成Android项目,所以我们需要配置app的build.gradle。因为gradle只是构建工具,它是根据不同的插件来构建不同的项目,所以为了符合Android的构建,需要申明应用的插件。这里通过apply方法,它有以下三种类型

void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)

这里我们使用的是第二种,它的map参数需要与ObjectConfigurationAction中的方法名相匹配,而它的方法名有以下三种

  • from: 应用一个脚本文件
  • plugin: 应用一个插件,通过id或者class名
  • to: 应用一个目标代理对象

因为我们要使用android插件,所以需要使用apply(plugin: 'com.android.application'),又由于groovy的语法特性,可以将括号省略,所以最终在build.gradle中的表现可以如下:

$ echo "apply plugin: 'com.android.application'" > app/build.gradle

添加完以后,再来执行一下

$ gradle app:tasks

FAILURE: Build failed with an exception.
 
* Where:
Build file '/Users/idisfkj/example/app/build.gradle' line: 1
 
* What went wrong:
A problem occurred evaluating project ':app'.
> Plugin with id 'com.android.application' not found.
 
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
 
* Get more help at https://help.gradle.org
 
BUILD FAILED in 6s

发现报错了,显示com.android.application的插件id找不到。这正常,因为我们还没有声明它。所以下面我们要在project下的build.gradle中声明它。为什么不直接到app下的build.gradle声明呢?是因为我们是android项目,project可以有多个sub-project,所以为了防止在子项目中重复声明,统一到主项目中声明。

project的build.gradle声明插件需要在buildscript中,而buildscript会通过ScriptHandler来执行,以至于sub-project也能够使用。所以最终的申明如下:

buildscript {
    repositories {
        google()
        jcenter()
    }
 
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.2'
    }
}

上面的buildscript、repositories与dependencies方法都是以Closure作为参数,然后再通过delegate进行调用

  • buildscript(Closure)在Project中调用,通过ScriptHandler来执行Closure
  • repositories(Closure)在ScriptHandler中调用,通过RepositoryHandler来执行Closure
  • dependencies(Closure)在ScriptHandler中调用,通过DependencyHandler来执行Closure

相应的google()与jcenter()会在RepositoryHandler执行,classpaht(String)会在DependencyHandler(*)执行。

如果你想更详细的了解可以查看文档

让我们再一次执行gradle projects

$ gradle projects

FAILURE: Build failed with an exception.
 
* What went wrong:
A problem occurred configuring project ':app'.
> compileSdkVersion is not specified.
 
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
 
* Get more help at https://help.gradle.org
 
BUILD FAILED in 1s

发现报没有指定compileSdkVersion,因为我们还没有对app进行相关的配置,只是引用了android插件。所以我们现在来进行基本配置,在app/build.gradle中添加

android {
   buildToolsVersion "28.0.1"
   compileSdkVersion 28
}

我们在android中进行声明,android方法会加入到project实例中。buildToolsVersion与compileSdkVersion将通过Closure对象进行delegate。

Extensions

android方法会是如何与project进行关联的?在我们声明的Android插件中,会注册一个AppExtension类,这个extension将会与android命名。所以gradle能够调用android方法,而在AppExtension中已经声明了各种方法属性,例如buildTypes、defaultConfig与signingConfigs等。这也就是为什么我们能够在android方法中调用它们的原因。下面是extension的创建部分源码

    @Override
    void apply(Project project) {
        super.apply(project)
        // This is for testing.
        if (pluginHolder != null) {
            pluginHolder.plugin = this;
        }
        def buildTypeContainer = project.container(DefaultBuildType,
                new BuildTypeFactory(instantiator,  project.fileResolver))
        def productFlavorContainer = project.container(GroupableProductFlavorDsl,
                new GroupableProductFlavorFactory(instantiator, project.fileResolver))
        def signingConfigContainer = project.container(SigningConfig,
                new SigningConfigFactory(instantiator))
        extension = project.extensions.create('android', AppExtension,
                this, (ProjectInternal) project, instantiator,
                buildTypeContainer, productFlavorContainer, signingConfigContainer)
        setBaseExtension(extension)
        ...
   }

Dependencies

android方法下面就是dependencies,下面我们再来看dependencies

dependencies {
    implementation 'io.reactivex.rxjava2:rxjava:2.0.4'
    testImplementation 'junit:junit:4.12'
    annotationProcessor 'org.parceler:parceler:1.1.6'
}

有了上面的基础,应该会容易理解。dependencies是会被delegate给DependencyHandler,不过如果你到DependencyHandler中去查找,会发现找不到上面的implementation、testImplementation等方法。那它们有到底是怎么来的呢?亦或者如果我们添加了dev flavor,那么我又可以使用devImplementation。这里就涉及到了groovy的methodMissing方法。它能够在runtime(*)中捕获到没有定义的方法。

至于(*)是gradle的methodMissing中的一个抽象感念,它申明在MethodMixIn中。

对于DependencyHandler的实现规则是:
在DependencyHandler中如果我们回调了一个没有定义的方法,且它有相应的参数;同时它的方法名在configuration(*)中;那么将会根据方法名与参数类型来调用doAdd的相应方法。

对于configuration(*),每一个plugin都有他们自己的配置,例如java插件定义了compile、compileClassPath、testCompile等。而对于Android插件在这基础上还会定义annotationProcessor,(variant)Implementation、(variant)TestImplementation等。对于variant则是基于你设置的buildTypes与flavors。

另一方面,由于doAdd()是私用的方法,但add()是公用的方法,所以在dependencies中我们可以直接使用add

dependencies {
    add('implementation', 'io.reactivex.rxjava2:rxjava:2.0.4')
    add('testImplementation', 'junit:junit:4.12')
    add('annotationProcessor', 'org.parceler:parceler:1.1.6')
}

注意,这种写法并不推荐,这里只是为了更好的理解它的原理。

gradle的知识点还有很多,这只是对有关Android的一部分进行分析。当我们进行gradle配置的时,不至于对gradle的语法感到魔幻,或者对它的一些操作感到不解。

我在github上建了一个仓库Android精华录,收集Android相关的文章,如果有需要的可以去看一下,有好的文章可以加我微信fan331100推荐给我。

clipboard.png

查看原文

赞 4 收藏 4 评论 0

午后一小憩 发布了文章 · 2019-05-10

Gson与List<T>对象间的相亲之旅

图片描述

随着人们的生活水平的提高,连带着人与人之间的相亲渠道也进一步改善。最近偶尔看到几档相亲的综艺节目,不管是平民还是明星。可见相亲的热潮正扑面而来。这不Google与Java两个老家伙也坐不住了,分别想着自己排行285的儿子Gson与自己排行570的女儿List<T>也该到了相亲的年龄了。于是Google与Java两个老油条会心一笑,一起策划了今天的这次相亲之旅。

地下恋情

Google回到家就将此事告诉了Gson,通知他明天就去Android Studio匆匆那年餐厅见面。Gson欲言欲止,好像另有隐情,但在父亲高大身躯与凌厉的眼神下答应了下来,而且父亲能够在这众多的儿子中想到自己,也不忍心拒绝父亲的善意。

Gson回到房,躺着床上,脑海回想起自己与Java排行520的女儿Object的地下恋情。

Gson与Object的第一次见面还是被它的Json字符串装扮所吸引。那一天她宛如一朵含苞待放的牡丹花,美而不妖,艳而不俗,千娇百媚,无与伦比。(以上均为Gson视角,请勿迷恋。以下为code视角)

{
    "marquee": {
        "content": "翠绿烟纱散花裙",
        "status": true
    }
}

这一下就激起了Gson的欲望,而且Gson还有点小得意,对于这种女孩他已经有自己的一套完整攻略方案。既然知道了它的Json字符串格式,就可以迅速创建出它对应的java类

public class HomeMarqueeModel {
 
    private MarqueeModel marquee;
 
    public static class MarqueeModel {
        private String content;
        private boolean status;
 
        public String getContent() {
            return content;
        }
 
        public boolean isStatus() {
            return status;
        }
    }
 
    public MarqueeModel getMarquee() {
        return marquee;
    }
}

然后再根据API攻略法则第3089条,使用fromJson方案进行攻略,成功率高达100%

HomeMarqueeModel model = new Gson().fromJson(jsonStr, HomeMarqueeModel.class);

就这样Gson完成了对Object的第一次攻略,获取到了Object的好感。但Gson不满足,为了完全让Object对自己死心塌地,必须应对Object的所有Json字符串格式。

回去之后,Gson在Android Studio微信平台与Java中的好哥们泛型T打探Object的特性。经过交流,发现T它刚好是这方面的能手,T告诉Gson每一个Object都有它独用的Class属性,为了代表所有的Class类型,刚好可以使用它的泛型T来表示,于是就有了Class<T>

第二天,Gson主动出击邀请Object去Android Studio匆匆那年餐厅吃饭。Gson还是使用它的fromJson方法,只是在这方法上加入了T的思想。

    public <T> T getObjectGirl(String jsonStr, Class<T> tGirl) {
        T girl = getGson().fromJson(jsonStr, tGirl);
        return girl;
    }

就这样,Gson完成对Object的所有类型的攻略,从此不再为女友而发愁。
回想结束,拉回到现实,对于明天的相亲,Gson打算先用之前的方法试一下,毕竟Gson经过前面的成功实例,还是有点小膨胀,

初次见面

早上9点,Gson整装待发,开着自己的兰博基尼向Android Studio匆匆那年餐厅进发。大约10点Gson到达餐厅,且已经选好了一处风景优雅且面朝大海的位置,静静的等候List的到来。半小时之后,只见一个身穿藕色纱衫的女孩,脸带微笑,身形苗条,长发披向背心,用一根银色丝带轻轻挽住,迎面而来。Gson望着她的身影,只觉这女孩身旁似有烟霞轻笼,当真非尘世中人。(以上均为Gson视角,请勿迷恋。以下为code视角)

[
    {
        "title": "身穿藕色纱衫",
        "url": "http://127.0.0.1:8000/admin2/operation/banner-list/",
        "status": true
    },
    {
        "title": "长发披向背心",
        "url": "http://127.0.0.1:8000/admin2/operation/banner-list/",
        "status": true
    }
]

为了保守起见,Gson决定还是按部就班来,首先创建出该Json字符串列表的java类

public class HomeBannerModel {
    private String title;
    private String url;
    private boolean status;
 
    public String getTitle() {
        return title;
    }
 
    public String getUrl() {
        return url;
    }
 
    public boolean isStatus() {
        return status;
    }
}

然后再使用fromJson方案进行攻略,稍微不同的是这里它是一个数组

HomeBannerModel[] array = new Gson().fromJson(jsonStr, HomeBannerModel[].class);
List<HomeBannerModel> list = Arrays.asList(array);

嗯,看样子效果不错,有进一步发展的机会。于是Gson又展示它的另一个攻略

Type type = new TypeToken<List<HomeBannerModel>>(){}.getType();
List<HomeBannerModel> list = new Gson().fromJson(jsonStr, type);

果然,Gson再一次成功逗笑了List。Gson的膨胀心再一次暴涨。Gson于是大胆起来,套用之前泛型T的思想。于是有了下面的第一次T尝试

clipboard.png

发现不行,不支持这种泛型T解析。既然这种不行,还就换另一种,于是就有了第二次T的尝试

public <T> List<T> getListGirl(String jsonStr) {
      Type type = new TypeToken<List<T>>(){}.getType();
      List<T> listGirl = new Gson().fromJson(jsonStr, type);
      return listGirl;
}

发现没问题,那么再来实践运行一下。发现会报如下异常,导致小姐姐不开心。

java.lang.AssertionError: illegal type variable reference

说明Gson解析不支持该泛型T书写,导致Type解析出错,Gson一下懵了,那该咋整呢?虽然前面的攻略有效果,但最后的尝试没有成功。但天色以晚,今天的相亲也只能就此打住,有待进一步商榷。

请教

回到家Gson一直挂念着这件事,一筹莫展。Google看到自己儿子愁眉苦展的样子,不经询问今天的进展。了解情况后,Google给Gson的建议是,可以去请教下ParameterizedType。于是Gson迫不及待的去找ParameterizedType学习人生真谛。

经过请教,发现ParameterizedType是继承于Type,自己另外提供了三个抽象方法,分别为

  1. Type[] getActualTypeArguments() 返回真正所需的Type类型数组
  2. Type getRawType() 返回原始的Type类型
  3. Type getOwnerType() 返回此类的成员类型,例如:O<T>.I<S>。如果为顶层类型,则返回null。

所以为了解决之前的问题,Gson打算先自定义一个GirlParameterizedType类,让它实现ParameterizedType接口。code如下:

    private static class GirlParameterizedType implements ParameterizedType {
 
        private Class aClass;
 
        GirlParameterizedType(Class aClass) {
            this.aClass = aClass;
        }
 
        @NonNull
        @Override
        public Type[] getActualTypeArguments() {
            return new Type[]{aClass};
        }
 
        @NonNull
        @Override
        public Type getRawType() {
            return List.class;
        }
 
        @Override
        public Type getOwnerType() {
            return null;
        }
    }

既然找到问题所在,Gson迫不及待的邀请List去逛Android Studio商城,希望明天能够顺利拿下List女神。

再次相见

在Android Studio商城,Gson再一次看到了List,只不过她今天已经换了一身装扮。只见她身穿粉红玫瑰香紧身袍袍袖上衣,下罩翠绿烟纱散花裙,腰间用金丝软烟罗系成一个大大的蝴蝶结,鬓发低垂斜插碧玉瓒凤钗,显的体态修长妖妖艳艳勾人魂魄。不过Gson已早有准备,直接步入主题,拿出昨天准备好的GirlParameterizedType。

public <T> List<T> getListGirl(String jsonStr, Class<T> tClass) {
      Type type =  getGson().fromJson(response, new HttpClientParameterizedType(tClass));
      List<T> listGirl = new Gson().fromJson(jsonStr, type);
      return listGirl;
}

发现是如此的简单,一击必中,直击List芳心。于是一小时之后,Gson双手已经挂满了商品,额头也满头大汗,但List还有意未尽的样子,Gson万万没想到最后居然败在购物上,果然带女孩来商城就是个错误的选择...

这次的相亲也算完美结束,只不过Gson心中又有了心的疑虑,对于Object与List都是百年难遇的女孩,该如何抉择呢?要不各位看官这个抉择就交给你们,相信你们会做出正确的抉择的,毕竟大家都不想在code的人生中留下一丝bug的身影。

最后,不知大家看的感受如何,有什么感受也可以反馈给我。如果喜欢这种方式,可以关注我的公众号:Android补给站,以便及时推送最新文章给你哟~

clipboard.png

查看原文

赞 6 收藏 4 评论 1