الملاحة في مشاريع متعددة الوحدات



يعد التنقل في تطوير تطبيقات Android أمرًا مهمًا ويجب أن تفكر مرتين في أكثر ما يناسب المكتبة (أو الحل الخاص بك) وكيف سيكون مناسبًا للاستخدام عندما يصبح التطبيق أكبر. أيضًا ، قد يكون من الجيد التفكير في مدى سهولة تغيير تطبيقك إلى تطبيق آخر.

قبل أن نبدأ ، اسمحوا لي أن أقول قصة. دعنا نسميها هكذا "كيف صنعنا وحدات المشروع ولماذا كرهت الملاحة لدينا."

كان لدينا مشروع وحدة واحدة وكل شيء سار على ما يرام باستثناء وقت البناء ، وذلك لأننا استخدمنا Dagger واستغرقنا وقتًا طويلاً لإنشاء مكون واحد. لذلك قررنا فصل المشروع إلى وحدات حسب الميزات ، على سبيل المثال ، ميزة تسجيل الدخول ، وميزة المساعدة ، إلخ.

واجهنا الكثير من الصعوبات لأننا اضطررنا إلى نقل منطق الشبكة إلى وحدة Gradle الخاصة به وتسجيل الدخول إلى المجال في وحدته وأيضًا لدينا مشاكل في التنقل.

في تطبيقنا ، كان لدينا جهاز توجيه يعرف كل جزء ، وكان مسؤولاً عن التحولات بينهما. لسوء الحظ ، عرفت كل جزء بوجود جهاز توجيه وعرف عن أجزاء أخرى ، لأن الجزء قال للموجه ما يجب فتح جزء منه أو أن الجزء ينتظر انتظار النتائج من جزء آخر. هذه الأشياء دمرت فكرة صنع وحدات ميزة مستقلة.

لذلك ، كنت أفكر في كيفية جعله أفضل ، وكيفية فصل هذه الروابط بين الأجزاء ، وكيفية التخلص من المعرفة بوجود بعض أجهزة التوجيه. في هذه المقالة ، سأبين ما صنعته.

ماذا سنفعل



سيكون التطبيق بسيطًا جدًا ولكنه سيكون معياريًا. ستكون هناك وحدة تطبيق واحدة وثلاث وحدات للميزات:

  • وحدة التطبيقات - هذه الوحدة تعرف كل شيء عن التطبيق وهناك سنقوم بتطبيق الملاحة لدينا. سوف نستخدم مكون التنقل من JetPack.
  • وحدة الأسئلة - هذه الوحدة مسؤولة عن ميزة الأسئلة. لا تعرف الوحدة شيئًا عن وحدة التطبيقات أو أي وحدة أخرى. سيكون هناك QuestionsNavigation وواجهة QuestionsNavigation .
  • وحدة السؤال - سيكون هناك ميزة السؤال. كما الوحدة السابقة ، فإنه لا يعرف عن وحدات أخرى. ستحتوي هذه الوحدة على QuestionNavigation وواجهة QuestionNavigation .
  • وحدة النتيجة - هذه هي ميزة النتيجة. أيضا ، هو وحدة مستقلة تماما. سيكون هناك RightAnswerFragment مع واجهة RightAnswerNavigation وسيكون هناك WrongAnswerFragment مع واجهة WrongAnswerNavigation .

يمكن العثور على الكود المصدري للتطبيق هنا: GitHub .

التنفيذ


في هذه المقالة ، سأستخدم مكتبة ComponentsManager للحصول على ميزة التنقل. في مشروع حقيقي ، سأوفر ميزة التنقل عن طريق Dagger ، لكن ليس من الضروري لهذا المشروع الصغير. أيضًا ، تساعدك مكتبة ComponentManager على استخدام Dagger في مشاريع متعددة الوحدات.

هل انت جاهز دعنا نذهب!

وحدة الأسئلة


أنشئ وحدة أندرويد واسميها الأسئلة . بعد ذلك ، تأكد من تكوين Kotlin في الوحدة النمطية وأن تتم إضافة ConstraintLayout و ComponentsManager تبعيات.

 //build.gradle (for the questions module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

بدلاً من استخدام Route أو Navigator في Fragment ، Fragment واجهة QuestionsNavigation تحدد التحولات المطلوبة. وبالتالي ، بالنسبة إلى Fragment ، لا يهم كيف سيتم تنفيذ تلك التحولات ، بل يحتاج فقط إلى تلك الواجهة ويعتمد عليها عند استدعاء الطريقة ، وستكون الشاشة المطلوبة مفتوحة.

 interface QuestionsNavigation { fun openQuestion(questionId: Long) } 

لفتح شاشة مع سؤال ، نحن فقط نسمي أسلوب openQuestion(questionId: Long) وهذا هو. نحن لا نهتم بكيفية فتح الشاشة ، ولا نهتم بما إذا كانت Fragment أو Fragment أو أي شيء آخر.

هنا هو QuestionsFragment وتخطيطه .

 class QuestionsFragment : Fragment() { private val navigation: QuestionsNavigation by lazy { XInjectionManager.findComponent<QuestionsNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_questions, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_first_question.setOnClickListener { navigation.openQuestion(1) } button_second_question.setOnClickListener { navigation.openQuestion(2) } button_third_question.setOnClickListener { navigation.openQuestion(3) } } } 

وحدة السؤال


أولاً ، قم بإنشاء وحدة مكتبة Android واسميها السؤال . يجب أن يحتوي build.gradle الخاص بالوحدة النمطية على نفس التبعيات مثل ملف build.gradle الخاص بوحدة الأسئلة.

 //build.gradle (for the question module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

بعد ذلك نقوم بإنشاء واجهة تعرف التنقل في الوحدة. ستكون فكرة التنقل في الوحدات النمطية الأخرى كما في النموذج السابق.

من مستخدم QuestionFragment يمكن فتح شاشة إجابة خاطئة أو شاشة إجابة صحيحة حتى يكون للواجهة طريقتان.

 interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() } 

آخر شيء هو شظية. بالنسبة إلى هذه الشريحة ، سنضيف طريقة مصاحبة تقوم بإرجاع Bundle حتى نتمكن من تمرير معرف السؤال واستخدامه. تخطيط الجزء.

 class QuestionFragment : Fragment() { companion object { private const val EXTRA_QUESTION_ID = "me.vponomarenko.modular.navigation.question.id" fun createBundle(questionId: Long) = Bundle().apply { putLong(EXTRA_QUESTION_ID, questionId) } } private val navigation: QuestionNavigation by lazy { XInjectionManager.findComponent<QuestionNavigation>() } private val questionId: Long by lazy { arguments?.getLong(EXTRA_QUESTION_ID) ?: throw IllegalStateException("no questionId") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_question, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) text_question.text = getString(R.string.question, questionId) button_right_answer.setOnClickListener { navigation.openRightAnswer() } button_wrong_answer.setOnClickListener { navigation.openWrongAnswer() } } } 

وحدة النتيجة


وحدة الميزة النهائية هي وحدة النتائج. سيكون هناك شظايا تظهر إجابة صحيحة وخاطئة. قم بإنشاء وحدة مكتبة Android وندعوها بالنتيجة ، ثم قم بتغيير build.gradle للوحدة بحيث يكون لها نفس التبعيات مثل وحدات الميزات السابقة.

 //build.gradle (for the result module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

لنبدأ من جزء الإجابة الصحيحة. سيكون لواجهة التنقل طريقة واحدة لأن المستخدم يمكنه فتح شاشة أسئلة فقط.

 interface RightAnswerNavigation { fun openAllQuestions() } 

The RightAnswerFragment ، تخطيطه .

 class RightAnswerFragment : Fragment() { private val navigation: RightAnswerNavigation by lazy { XInjectionManager.findComponent<RightAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_right, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_all_questions.setOnClickListener { navigation.openAllQuestions() } } } 

كما قلت سابقًا ، تحتوي هذه الوحدة على Fragments ، لذلك دعونا ننفذ شاشة إجابة خاطئة.

في تطبيقي ، من مستخدم شاشة الإجابة الخاطئة ، يمكن فقط الرجوع إلى شاشة السؤال ومحاولة الإجابة على السؤال مرة أخرى ، وبالتالي فإن واجهة التنقل لديها طريقة واحدة فقط.

 interface WrongAnswerNavigation { fun tryAgain() } 

و WrongAnswerFragment ، تخطيطه .

 class WrongAnswerFragment : Fragment() { private val navigation: WrongAnswerNavigation by lazy { XInjectionManager.findComponent<WrongAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_wrong, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_try_again.setOnClickListener { navigation.tryAgain() } } } 

وحدة التطبيق


لقد صنعنا الوحدات النمطية وحان الوقت لتوصيلها وتشغيل التطبيق.

أول شيء أولاً ، نحن بحاجة إلى تعديل build.gradle في وحدة التطبيق. يجب أن يحتوي على كافة الوحدات النمطية التي تم إنشاؤها ومكتبة مكون التنقل مع إدارة المكونات.

 //build.gradle (for the app module) apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation project(':questions') implementation project(':question') implementation project(':result') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'com.github.valeryponomarenko.componentsmanager:androidx:2.0.1' implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha11' ... } 

قبل تنفيذ الفئات التي تعمل مع الملاحة ، يتعين علينا إضافة التنقل نفسه. سوف نستخدم مكون التنقل لتصميمه.

قم بإنشاء ملف موارد تنقل واستدعاه nav_graph.xml . سيكون هناك ثلاث اتصالات:

  • the QuestionFragment مع QuestionFragment
  • The QuestionFragment مع WrongAnswerFragment
  • The QuestionFragment مع RightAnwerFragment . هذا الصدد لديه اختلاف بسيط. إذا كان المستخدم على RightAnswerFragment وكان المستخدم يضغط على زر الرجوع ، فسيتم إعادته إلى QuestionsFragment . كيف يحدث ذلك؟ ما عليك rightAnswerFragment اختيار السهم الذي يربط questionFragment مع rightAnswerFragment ، ثم في القائمة المنسدلة بالقرب من Pop لتحديد questionsFragment .


هنا هو تمثيل XML للرسم البياني للتنقل.

 <?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" android:id="@+id/nav_graph" app:startDestination="@id/questionsFragment"> <fragment android:id="@+id/questionsFragment" android:name="me.vponomarenko.modular.navigation.questions.QuestionsFragment" android:label="QuestionsFragment"> <action android:id="@+id/action_questionsFragment_to_questionFragment" app:destination="@id/questionFragment" /> </fragment> <fragment android:id="@+id/questionFragment" android:name="me.vponomarenko.modular.navigation.question.QuestionFragment" android:label="QuestionFragment"> <action android:id="@+id/action_questionFragment_to_wrongAnswerFragment" app:destination="@id/wrongAnswerFragment" /> <action android:id="@+id/action_questionFragment_to_rightAnswerFragment" app:destination="@id/rightAnswerFragment" app:popUpTo="@+id/questionsFragment" /> </fragment> <fragment android:id="@+id/wrongAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.wrong.WrongAnswerFragment" android:label="WrongAnswerFragment" /> <fragment android:id="@+id/rightAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.right.RightAnswerFragment" android:label="RightAnswerFragment" /> </navigation> 

ثم قم بإنشاء فئة Navigator . من الواضح أن الفصل مسؤول عن إجراء انتقالات بين الشظايا. يقوم بتنفيذ جميع واجهات التنقل من الوحدات النمطية للميزات ، ولا تنس إضافة مكالمات من الإجراءات المقابلة لفتح الشاشة المطلوبة. أيضًا ، لدى الفصل طرق لربط navController .

 class Navigator : QuestionsNavigation, QuestionNavigation, RightAnswerNavigation, WrongAnswerNavigation { private var navController: NavController? = null override fun openQuestion(questionId: Long) { navController?.navigate( R.id.action_questionsFragment_to_questionFragment, QuestionFragment.createBundle(questionId) ) } override fun openWrongAnswer() { navController?.navigate(R.id.action_questionFragment_to_wrongAnswerFragment) } override fun openRightAnswer() { navController?.navigate(R.id.action_questionFragment_to_rightAnswerFragment) } override fun openAllQuestions() { navController?.popBackStack() } override fun tryAgain() { navController?.popBackStack() } fun bind(navController: NavController) { this.navController = navController } fun unbind() { navController = null } } 

في السطر الثامن ، يمكنك رؤية كيفية تمرير معرف QuestionFragment إلى QuestionFragment .

بعد ذلك ، قم بإنشاء فصل NavApplication . في الواقع ، اضطررت إلى إضافة هذه الفئة لتوفير Navigator للفئات الأخرى ، وإلا ، سيكون من الصعب الحصول على المستكشف في الوحدات النمطية للميزات. لا تنس إضافة هذه الفئة إلى البيان.

 class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } } 

باستخدام الأسطر 4 إلى 6 ، سيكون من الممكن الحصول على تطبيقات واجهة التنقل الخاصة بالميزات عن طريق استدعاء XInjectionManager.findComponent ().

أخيرًا وليس آخرًا ، قم بتغيير فئة MainActivity وتخطيطها .

 class MainActivity : AppCompatActivity() { private val navigator: Navigator by lazy { XInjectionManager.findComponent<Navigator>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onResume() { super.onResume() navigator.bind(findNavController(R.id.nav_host_fragment)) } override fun onPause() { super.onPause() navigator.unbind() } override fun onSupportNavigateUp(): Boolean = findNavController(R.id.nav_host_fragment).navigateUp() } 

هذا هو كل شيء ، الآن يمكنك تشغيل التطبيق ومعرفة كيفية عمله.

ملخص


عمل جيد! لقد صنعنا نظام ملاحة:

  • تقطع الاتصالات بين الأجزاء في وحدات الميزات المختلفة ،
  • يمكن تغييرها بسهولة ،
  • من السهل اختباره.

Crosspost

Source: https://habr.com/ru/post/ar443288/


All Articles